add prettier
This commit is contained in:
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
62994
package-lock.json
generated
62994
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
196
package.json
196
package.json
@@ -1,99 +1,101 @@
|
|||||||
{
|
{
|
||||||
"name": "noauth",
|
"name": "noauth",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@mui/icons-material": "^5.14.19",
|
"@mui/icons-material": "^5.14.19",
|
||||||
"@mui/material": "^5.14.20",
|
"@mui/material": "^5.14.20",
|
||||||
"@nostr-dev-kit/ndk": "^2.4.0",
|
"@nostr-dev-kit/ndk": "^2.4.0",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^17.0.45",
|
"@types/node": "^17.0.45",
|
||||||
"@types/react": "^18.2.38",
|
"@types/react": "^18.2.38",
|
||||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"nostr-tools": "^1.17.0",
|
"nostr-tools": "^1.17.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.50.0",
|
"react-hook-form": "^7.50.0",
|
||||||
"react-redux": "^9.0.3",
|
"react-redux": "^9.0.3",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"workbox-background-sync": "^6.6.0",
|
"workbox-background-sync": "^6.6.0",
|
||||||
"workbox-broadcast-update": "^6.6.0",
|
"workbox-broadcast-update": "^6.6.0",
|
||||||
"workbox-cacheable-response": "^6.5.4",
|
"workbox-cacheable-response": "^6.5.4",
|
||||||
"workbox-core": "^6.6.0",
|
"workbox-core": "^6.6.0",
|
||||||
"workbox-expiration": "^6.6.0",
|
"workbox-expiration": "^6.6.0",
|
||||||
"workbox-google-analytics": "^6.6.0",
|
"workbox-google-analytics": "^6.6.0",
|
||||||
"workbox-navigation-preload": "^6.6.0",
|
"workbox-navigation-preload": "^6.6.0",
|
||||||
"workbox-precaching": "^6.6.0",
|
"workbox-precaching": "^6.6.0",
|
||||||
"workbox-range-requests": "^6.6.0",
|
"workbox-range-requests": "^6.6.0",
|
||||||
"workbox-routing": "^6.6.0",
|
"workbox-routing": "^6.6.0",
|
||||||
"workbox-strategies": "^6.6.0",
|
"workbox-strategies": "^6.6.0",
|
||||||
"workbox-streams": "^6.6.0",
|
"workbox-streams": "^6.6.0",
|
||||||
"yup": "^1.3.3"
|
"yup": "^1.3.3"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react-scripts": {
|
"react-scripts": {
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-app-rewired build",
|
"build": "react-app-rewired build",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-app-rewired eject",
|
"eject": "react-app-rewired eject",
|
||||||
"serve": "npm run build && serve -s build"
|
"serve": "npm run build && serve -s build",
|
||||||
},
|
"format": "npx prettier --write src"
|
||||||
"eslintConfig": {
|
},
|
||||||
"extends": [
|
"eslintConfig": {
|
||||||
"react-app",
|
"extends": [
|
||||||
"react-app/jest"
|
"react-app",
|
||||||
]
|
"react-app/jest"
|
||||||
},
|
]
|
||||||
"browserslist": {
|
},
|
||||||
"production": [
|
"browserslist": {
|
||||||
">0.2%",
|
"production": [
|
||||||
"not dead",
|
">0.2%",
|
||||||
"not op_mini all"
|
"not dead",
|
||||||
],
|
"not op_mini all"
|
||||||
"development": [
|
],
|
||||||
"last 1 chrome version",
|
"development": [
|
||||||
"last 1 firefox version",
|
"last 1 chrome version",
|
||||||
"last 1 safari version"
|
"last 1 firefox version",
|
||||||
]
|
"last 1 safari version"
|
||||||
},
|
]
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"devDependencies": {
|
||||||
"assert": "^2.1.0",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"buffer": "^6.0.3",
|
"assert": "^2.1.0",
|
||||||
"crypto-browserify": "^3.12.0",
|
"buffer": "^6.0.3",
|
||||||
"customize-cra": "^1.0.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"https-browserify": "^1.0.0",
|
"customize-cra": "^1.0.0",
|
||||||
"os-browserify": "^0.3.0",
|
"https-browserify": "^1.0.0",
|
||||||
"process": "^0.11.10",
|
"os-browserify": "^0.3.0",
|
||||||
"react-app-rewired": "^2.2.1",
|
"prettier": "^3.2.5",
|
||||||
"serve": "^14.2.1",
|
"process": "^0.11.10",
|
||||||
"stream-browserify": "^3.0.0",
|
"react-app-rewired": "^2.2.1",
|
||||||
"stream-http": "^3.2.0",
|
"serve": "^14.2.1",
|
||||||
"url": "^0.11.3"
|
"stream-browserify": "^3.0.0",
|
||||||
}
|
"stream-http": "^3.2.0",
|
||||||
|
"url": "^0.11.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react'
|
||||||
import App from './App';
|
import App from './App'
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
test('renders learn react link', () => {
|
||||||
render(<App />);
|
render(<App />)
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
const linkElement = screen.getByText(/learn react/i)
|
||||||
expect(linkElement).toBeInTheDocument();
|
expect(linkElement).toBeInTheDocument()
|
||||||
});
|
})
|
||||||
|
|||||||
139
src/App.tsx
139
src/App.tsx
@@ -2,12 +2,7 @@ import { DbKey, dbi } from './modules/db'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { swicOnRender } from './modules/swic'
|
import { swicOnRender } from './modules/swic'
|
||||||
import { useAppDispatch } from './store/hooks/redux'
|
import { useAppDispatch } from './store/hooks/redux'
|
||||||
import {
|
import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
|
||||||
setApps,
|
|
||||||
setKeys,
|
|
||||||
setPending,
|
|
||||||
setPerms,
|
|
||||||
} from './store/reducers/content.slice'
|
|
||||||
import AppRoutes from './routes/AppRoutes'
|
import AppRoutes from './routes/AppRoutes'
|
||||||
import { fetchProfile, ndk } from './modules/nostr'
|
import { fetchProfile, ndk } from './modules/nostr'
|
||||||
import { useModalSearchParams } from './hooks/useModalSearchParams'
|
import { useModalSearchParams } from './hooks/useModalSearchParams'
|
||||||
@@ -18,86 +13,86 @@ import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
|
|||||||
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [render, setRender] = useState(0)
|
const [render, setRender] = useState(0)
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
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')
|
console.log(keys, 'keys')
|
||||||
|
|
||||||
dispatch(setKeys({ keys }))
|
dispatch(setKeys({ keys }))
|
||||||
const loadProfiles = async () => {
|
const loadProfiles = async () => {
|
||||||
const newKeys = []
|
const newKeys = []
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
// make it async
|
// make it async
|
||||||
const response = await fetchProfile(key.npub)
|
const response = await fetchProfile(key.npub)
|
||||||
if (!response) {
|
if (!response) {
|
||||||
newKeys.push(key)
|
newKeys.push(key)
|
||||||
} else {
|
} else {
|
||||||
newKeys.push({ ...key, profile: response })
|
newKeys.push({ ...key, profile: response })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(setKeys({ keys: newKeys }))
|
dispatch(setKeys({ keys: newKeys }))
|
||||||
}
|
}
|
||||||
// async load to avoid blocking main code below
|
// async load to avoid blocking main code below
|
||||||
loadProfiles()
|
loadProfiles()
|
||||||
|
|
||||||
const apps = await dbi.listApps()
|
const apps = await dbi.listApps()
|
||||||
dispatch(
|
dispatch(
|
||||||
setApps({
|
setApps({
|
||||||
apps: apps.map((app) => ({
|
apps: apps.map((app) => ({
|
||||||
...app,
|
...app,
|
||||||
// MOCK IMAGE
|
// MOCK IMAGE
|
||||||
icon: 'https://nostr.band/android-chrome-192x192.png',
|
icon: 'https://nostr.band/android-chrome-192x192.png',
|
||||||
})),
|
})),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const perms = await dbi.listPerms()
|
const perms = await dbi.listPerms()
|
||||||
dispatch(setPerms({ perms }))
|
dispatch(setPerms({ perms }))
|
||||||
|
|
||||||
const pending = await dbi.listPending()
|
const pending = await dbi.listPending()
|
||||||
dispatch(setPending({ pending }))
|
dispatch(setPending({ pending }))
|
||||||
|
|
||||||
// rerender
|
// rerender
|
||||||
// setRender((r) => r + 1)
|
// setRender((r) => r + 1)
|
||||||
|
|
||||||
if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected) load()
|
if (isConnected) load()
|
||||||
}, [render, isConnected, load])
|
}, [render, isConnected, load])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ndk.connect().then(() => {
|
ndk.connect().then(() => {
|
||||||
console.log('NDK connected', { ndk })
|
console.log('NDK connected', { ndk })
|
||||||
setIsConnected(true)
|
setIsConnected(true)
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// subscribe to updates from the service worker
|
// subscribe to updates from the service worker
|
||||||
swicOnRender(() => {
|
swicOnRender(() => {
|
||||||
console.log('render')
|
console.log('render')
|
||||||
setRender((r) => r + 1)
|
setRender((r) => r + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
<ModalInitial />
|
<ModalInitial />
|
||||||
<ModalImportKeys />
|
<ModalImportKeys />
|
||||||
<ModalSignUp />
|
<ModalSignUp />
|
||||||
<ModalLogin />
|
<ModalLogin />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import { ReactComponent as UnchekedLightIcon } from './icons/unchecked-light.svg
|
|||||||
import { default as AddImageIcon } from './icons/add-image.svg'
|
import { default as AddImageIcon } from './icons/add-image.svg'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AppLogo,
|
AppLogo,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
CheckmarkIcon,
|
CheckmarkIcon,
|
||||||
CheckedIcon,
|
CheckedIcon,
|
||||||
CheckedLightIcon,
|
CheckedLightIcon,
|
||||||
UnchekedIcon,
|
UnchekedIcon,
|
||||||
UnchekedLightIcon,
|
UnchekedLightIcon,
|
||||||
AddImageIcon,
|
AddImageIcon,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,156 +12,122 @@ import { useState } from 'react'
|
|||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { ACTION_TYPE } from '@/utils/consts'
|
import { ACTION_TYPE } from '@/utils/consts'
|
||||||
|
|
||||||
|
|
||||||
export const ModalConfirmConnect = () => {
|
export const ModalConfirmConnect = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||||
|
|
||||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(
|
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
|
||||||
ACTION_TYPE.BASIC,
|
|
||||||
)
|
|
||||||
|
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const appNpub = searchParams.get('appNpub') || ''
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
const pendingReqId = searchParams.get('reqId') || ''
|
const pendingReqId = searchParams.get('reqId') || ''
|
||||||
const isPopup = searchParams.get('popup') === 'true'
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
|
||||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
const { name, icon = '' } = triggerApp || {}
|
const { name, icon = '' } = triggerApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appName = name || getShortenNpub(appNpub)
|
||||||
|
|
||||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
return setSelectedActionType(value)
|
return setSelectedActionType(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseModal = createHandleCloseReplace(
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
onClose: async (sp) => {
|
||||||
{
|
sp.delete('appNpub')
|
||||||
onClose: async (sp) => {
|
sp.delete('reqId')
|
||||||
sp.delete('appNpub')
|
await swicCall('confirm', pendingReqId, false, false)
|
||||||
sp.delete('reqId')
|
},
|
||||||
await swicCall('confirm', pendingReqId, false, false)
|
})
|
||||||
}
|
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
},
|
onClose: (sp) => {
|
||||||
)
|
sp.delete('appNpub')
|
||||||
const closeModalAfterRequest = createHandleCloseReplace(
|
sp.delete('reqId')
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
},
|
||||||
{
|
})
|
||||||
onClose: (sp) => {
|
|
||||||
sp.delete('appNpub')
|
|
||||||
sp.delete('reqId')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function confirmPending(
|
async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) {
|
||||||
id: string,
|
call(async () => {
|
||||||
allow: boolean,
|
await swicCall('confirm', id, allow, remember, options)
|
||||||
remember: boolean,
|
console.log('confirmed', id, allow, remember, options)
|
||||||
options?: any
|
closeModalAfterRequest()
|
||||||
) {
|
})
|
||||||
call(async () => {
|
if (isPopup) window.close()
|
||||||
await swicCall('confirm', id, allow, remember, options)
|
}
|
||||||
console.log('confirmed', id, allow, remember, options)
|
|
||||||
closeModalAfterRequest()
|
|
||||||
})
|
|
||||||
if (isPopup) window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const allow = () => {
|
const allow = () => {
|
||||||
const options: any = {};
|
const options: any = {}
|
||||||
if (selectedActionType === ACTION_TYPE.BASIC)
|
if (selectedActionType === ACTION_TYPE.BASIC) options.perms = [ACTION_TYPE.BASIC]
|
||||||
options.perms = [ACTION_TYPE.BASIC];
|
// else
|
||||||
// else
|
// options.perms = ['connect','get_public_key'];
|
||||||
// options.perms = ['connect','get_public_key'];
|
confirmPending(pendingReqId, true, true, options)
|
||||||
confirmPending(pendingReqId, true, true, options)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const disallow = () => {
|
const disallow = () => {
|
||||||
confirmPending(pendingReqId, false, true)
|
confirmPending(pendingReqId, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPopup) {
|
if (isPopup) {
|
||||||
document.addEventListener('visibilitychange', function() {
|
document.addEventListener('visibilitychange', function () {
|
||||||
if (document.visibilityState == 'hidden') {
|
if (document.visibilityState == 'hidden') {
|
||||||
disallow();
|
disallow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal open={isModalOpened} withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}>
|
||||||
open={isModalOpened}
|
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||||
withCloseButton={!isPopup}
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||||
onClose={!isPopup ? handleCloseModal : undefined}
|
<Avatar
|
||||||
>
|
variant="square"
|
||||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
sx={{
|
||||||
<Stack
|
width: 56,
|
||||||
direction={'row'}
|
height: 56,
|
||||||
gap={'1rem'}
|
}}
|
||||||
alignItems={'center'}
|
src={icon}
|
||||||
marginBottom={'1rem'}
|
/>
|
||||||
>
|
<Box>
|
||||||
<Avatar
|
<Typography variant="h5" fontWeight={600}>
|
||||||
variant='square'
|
{appName}
|
||||||
sx={{
|
</Typography>
|
||||||
width: 56,
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
height: 56,
|
Would like to connect to your account
|
||||||
}}
|
</Typography>
|
||||||
src={icon}
|
</Box>
|
||||||
/>
|
</Stack>
|
||||||
<Box>
|
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||||||
<Typography variant='h5' fontWeight={600}>
|
<ActionToggleButton
|
||||||
{appName}
|
value={ACTION_TYPE.BASIC}
|
||||||
</Typography>
|
title="Basic permissions"
|
||||||
<Typography variant='body2' color={'GrayText'}>
|
description="Read your public key, sign notes and reactions"
|
||||||
Would like to connect to your account
|
// hasinfo
|
||||||
</Typography>
|
/>
|
||||||
</Box>
|
{/* <ActionToggleButton
|
||||||
</Stack>
|
|
||||||
<StyledToggleButtonsGroup
|
|
||||||
value={selectedActionType}
|
|
||||||
onChange={handleActionTypeChange}
|
|
||||||
exclusive
|
|
||||||
>
|
|
||||||
<ActionToggleButton
|
|
||||||
value={ACTION_TYPE.BASIC}
|
|
||||||
title='Basic permissions'
|
|
||||||
description='Read your public key, sign notes and reactions'
|
|
||||||
// hasinfo
|
|
||||||
/>
|
|
||||||
{/* <ActionToggleButton
|
|
||||||
value={ACTION_TYPE.ADVANCED}
|
value={ACTION_TYPE.ADVANCED}
|
||||||
title='Advanced'
|
title='Advanced'
|
||||||
description='Use for trusted apps only'
|
description='Use for trusted apps only'
|
||||||
hasinfo
|
hasinfo
|
||||||
/> */}
|
/> */}
|
||||||
<ActionToggleButton
|
<ActionToggleButton
|
||||||
value={ACTION_TYPE.CUSTOM}
|
value={ACTION_TYPE.CUSTOM}
|
||||||
title='On demand'
|
title="On demand"
|
||||||
description='Assign permissions when the app asks for them'
|
description="Assign permissions when the app asks for them"
|
||||||
/>
|
/>
|
||||||
</StyledToggleButtonsGroup>
|
</StyledToggleButtonsGroup>
|
||||||
<Stack direction={'row'} gap={'1rem'}>
|
<Stack direction={'row'} gap={'1rem'}>
|
||||||
<StyledButton
|
<StyledButton onClick={disallow} varianttype="secondary">
|
||||||
onClick={disallow}
|
Disallow
|
||||||
varianttype='secondary'
|
</StyledButton>
|
||||||
>
|
<StyledButton fullWidth onClick={allow}>
|
||||||
Disallow
|
{/* Allow {selectedActionType} actions */}
|
||||||
</StyledButton>
|
Connect
|
||||||
<StyledButton
|
</StyledButton>
|
||||||
fullWidth
|
</Stack>
|
||||||
onClick={allow}
|
</Stack>
|
||||||
>
|
</Modal>
|
||||||
{/* Allow {selectedActionType} actions */}
|
)
|
||||||
Connect
|
|
||||||
</StyledButton>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||||
import {
|
import { ToggleButtonGroup, ToggleButtonGroupProps, styled } from '@mui/material'
|
||||||
ToggleButtonGroup,
|
|
||||||
ToggleButtonGroupProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export const StyledButton = styled((props: AppButtonProps) => (
|
export const StyledButton = styled((props: AppButtonProps) => <Button {...props} />)(() => ({
|
||||||
<Button {...props} />
|
borderRadius: '19px',
|
||||||
))(() => ({
|
fontWeight: 600,
|
||||||
borderRadius: '19px',
|
padding: '0.75rem 1rem',
|
||||||
fontWeight: 600,
|
maxHeight: '41px',
|
||||||
padding: '0.75rem 1rem',
|
|
||||||
maxHeight: '41px',
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const StyledToggleButtonsGroup = styled(
|
export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />)(
|
||||||
(props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />,
|
() => ({
|
||||||
)(() => ({
|
gap: '0.75rem',
|
||||||
gap: '0.75rem',
|
marginBottom: '1rem',
|
||||||
marginBottom: '1rem',
|
justifyContent: 'space-between',
|
||||||
justifyContent: 'space-between',
|
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': {
|
||||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)':
|
margin: '0',
|
||||||
{
|
border: 'initial',
|
||||||
margin: '0',
|
},
|
||||||
border: 'initial',
|
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': {
|
||||||
},
|
border: 'initial',
|
||||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': {
|
borderRadius: '1rem',
|
||||||
border: 'initial',
|
},
|
||||||
borderRadius: '1rem',
|
})
|
||||||
},
|
)
|
||||||
}))
|
|
||||||
|
|||||||
@@ -3,30 +3,23 @@ import { ToggleButtonProps, Typography } from '@mui/material'
|
|||||||
import { StyledToggleButton } from './styled'
|
import { StyledToggleButton } from './styled'
|
||||||
|
|
||||||
type ActionToggleButtonProps = ToggleButtonProps & {
|
type ActionToggleButtonProps = ToggleButtonProps & {
|
||||||
description?: string
|
description?: string
|
||||||
hasinfo?: boolean
|
hasinfo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({
|
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
|
||||||
hasinfo = false,
|
const { title, description = '' } = props
|
||||||
...props
|
return (
|
||||||
}) => {
|
<StyledToggleButton {...props}>
|
||||||
const { title, description = '' } = props
|
<Typography variant="body2">{title}</Typography>
|
||||||
return (
|
<Typography className="description" variant="caption" color={'GrayText'}>
|
||||||
<StyledToggleButton {...props}>
|
{description}
|
||||||
<Typography variant='body2'>{title}</Typography>
|
</Typography>
|
||||||
<Typography
|
{hasinfo && (
|
||||||
className='description'
|
<Typography className="info" color={'GrayText'}>
|
||||||
variant='caption'
|
Info
|
||||||
color={'GrayText'}
|
</Typography>
|
||||||
>
|
)}
|
||||||
{description}
|
</StyledToggleButton>
|
||||||
</Typography>
|
)
|
||||||
{hasinfo && (
|
|
||||||
<Typography className='info' color={'GrayText'}>
|
|
||||||
Info
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</StyledToggleButton>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { ToggleButton, ToggleButtonProps, styled } from '@mui/material'
|
import { ToggleButton, ToggleButtonProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledToggleButton = styled((props: ToggleButtonProps) => (
|
export const StyledToggleButton = styled((props: ToggleButtonProps) => (
|
||||||
<ToggleButton classes={{ selected: 'selected' }} {...props} />
|
<ToggleButton classes={{ selected: 'selected' }} {...props} />
|
||||||
))(({ theme }) => ({
|
))(({ theme }) => ({
|
||||||
'&:is(&, :hover, :active)': {
|
'&:is(&, :hover, :active)': {
|
||||||
background: theme.palette.backgroundSecondary.default,
|
background: theme.palette.backgroundSecondary.default,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
flex: '1 0 6.25rem',
|
flex: '1 0 6.25rem',
|
||||||
height: '100px',
|
height: '100px',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
border: `2px solid transparent !important`,
|
border: `2px solid transparent !important`,
|
||||||
'&.selected': {
|
'&.selected': {
|
||||||
border: `2px solid ${theme.palette.text.primary} !important`,
|
border: `2px solid ${theme.palette.text.primary} !important`,
|
||||||
},
|
},
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
textTransform: 'initial',
|
textTransform: 'initial',
|
||||||
'& .description': {
|
'& .description': {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
lineHeight: '15px',
|
lineHeight: '15px',
|
||||||
margin: '0.5rem 0 0.25rem',
|
margin: '0.5rem 0 0.25rem',
|
||||||
},
|
},
|
||||||
'& .info': {
|
'& .info': {
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,26 +2,13 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
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 { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
|
import { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
|
||||||
import {
|
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
|
||||||
Avatar,
|
|
||||||
Box,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Stack,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material'
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom'
|
import { useParams, useSearchParams } from 'react-router-dom'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectAppsByNpub } from '@/store'
|
import { selectAppsByNpub } from '@/store'
|
||||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||||
import { FC, useEffect, useMemo, useState } from 'react'
|
import { FC, useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import { StyledActionsListContainer, StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||||
StyledActionsListContainer,
|
|
||||||
StyledButton,
|
|
||||||
StyledToggleButtonsGroup,
|
|
||||||
} from './styled'
|
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||||
@@ -30,209 +17,161 @@ import { ACTIONS } from '@/utils/consts'
|
|||||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||||
|
|
||||||
enum ACTION_TYPE {
|
enum ACTION_TYPE {
|
||||||
ALWAYS = 'ALWAYS',
|
ALWAYS = 'ALWAYS',
|
||||||
ONCE = 'ONCE',
|
ONCE = 'ONCE',
|
||||||
ALLOW_ALL = 'ALLOW_ALL',
|
ALLOW_ALL = 'ALLOW_ALL',
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_LABELS = {
|
const ACTION_LABELS = {
|
||||||
[ACTION_TYPE.ALWAYS]: 'Always',
|
[ACTION_TYPE.ALWAYS]: 'Always',
|
||||||
[ACTION_TYPE.ONCE]: 'Just Once',
|
[ACTION_TYPE.ONCE]: 'Just Once',
|
||||||
[ACTION_TYPE.ALLOW_ALL]: 'All Advanced Actions',
|
[ACTION_TYPE.ALLOW_ALL]: 'All Advanced Actions',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModalConfirmEventProps = {
|
type ModalConfirmEventProps = {
|
||||||
confirmEventReqs: IPendingsByAppNpub
|
confirmEventReqs: IPendingsByAppNpub
|
||||||
}
|
}
|
||||||
|
|
||||||
type PendingRequest = DbPending & { checked: boolean }
|
type PendingRequest = DbPending & { checked: boolean }
|
||||||
|
|
||||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs }) => {
|
||||||
confirmEventReqs,
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
}) => {
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
|
|
||||||
const appNpub = searchParams.get('appNpub') || ''
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
const isPopup = searchParams.get('popup') === 'true'
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||||
|
|
||||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(
|
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
|
||||||
ACTION_TYPE.ALWAYS,
|
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
|
||||||
)
|
|
||||||
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
|
|
||||||
|
|
||||||
const currentAppPendingReqs = useMemo(
|
const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
|
||||||
() => confirmEventReqs[appNpub]?.pending || [],
|
|
||||||
[confirmEventReqs, appNpub],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPendingRequests(
|
setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })))
|
||||||
currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })),
|
}, [currentAppPendingReqs])
|
||||||
)
|
|
||||||
}, [currentAppPendingReqs])
|
|
||||||
|
|
||||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
const { name, icon = '' } = triggerApp || {}
|
const { name, icon = '' } = triggerApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appName = name || getShortenNpub(appNpub)
|
||||||
|
|
||||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
return setSelectedActionType(value)
|
return setSelectedActionType(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
||||||
|
|
||||||
const handleCloseModal = createHandleCloseReplace(
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
onClose: (sp) => {
|
||||||
{
|
sp.delete('appNpub')
|
||||||
onClose: (sp) => {
|
sp.delete('reqId')
|
||||||
sp.delete('appNpub')
|
selectedPendingRequests.forEach(async (req) => await swicCall('confirm', req.id, false, false))
|
||||||
sp.delete('reqId')
|
},
|
||||||
selectedPendingRequests.forEach(
|
})
|
||||||
async (req) => await swicCall('confirm', req.id, false, false),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const closeModalAfterRequest = createHandleCloseReplace(
|
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
onClose: (sp) => {
|
||||||
{
|
sp.delete('appNpub')
|
||||||
onClose: (sp) => {
|
sp.delete('reqId')
|
||||||
sp.delete('appNpub')
|
},
|
||||||
sp.delete('reqId')
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function confirmPending(allow: boolean) {
|
async function confirmPending(allow: boolean) {
|
||||||
selectedPendingRequests.forEach((req) => {
|
selectedPendingRequests.forEach((req) => {
|
||||||
call(async () => {
|
call(async () => {
|
||||||
const remember = selectedActionType !== ACTION_TYPE.ONCE
|
const remember = selectedActionType !== ACTION_TYPE.ONCE
|
||||||
await swicCall('confirm', req.id, allow, remember)
|
await swicCall('confirm', req.id, allow, remember)
|
||||||
console.log('confirmed', req.id, selectedActionType, allow)
|
console.log('confirmed', req.id, selectedActionType, allow)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
closeModalAfterRequest()
|
closeModalAfterRequest()
|
||||||
if (isPopup) window.close();
|
if (isPopup) window.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeCheckbox = (reqId: string) => () => {
|
const handleChangeCheckbox = (reqId: string) => () => {
|
||||||
const newPendingRequests = pendingRequests.map((req) => {
|
const newPendingRequests = pendingRequests.map((req) => {
|
||||||
if (req.id === reqId) return { ...req, checked: !req.checked }
|
if (req.id === reqId) return { ...req, checked: !req.checked }
|
||||||
return req
|
return req
|
||||||
})
|
})
|
||||||
setPendingRequests(newPendingRequests)
|
setPendingRequests(newPendingRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAction = (req: PendingRequest) => {
|
const getAction = (req: PendingRequest) => {
|
||||||
const action = ACTIONS[req.method]
|
const action = ACTIONS[req.method]
|
||||||
if (req.method === 'sign_event') {
|
if (req.method === 'sign_event') {
|
||||||
const kind = getSignReqKind(req)
|
const kind = getSignReqKind(req)
|
||||||
if (kind !== undefined) return `${action} of kind ${kind}`
|
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||||
}
|
}
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPopup) {
|
if (isPopup) {
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState == 'hidden') {
|
if (document.visibilityState == 'hidden') {
|
||||||
confirmPending(false);
|
confirmPending(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal open={isModalOpened} withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}>
|
||||||
open={isModalOpened}
|
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||||
withCloseButton={!isPopup}
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||||
onClose={!isPopup ? handleCloseModal : undefined}
|
<Avatar
|
||||||
>
|
variant="square"
|
||||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
sx={{
|
||||||
<Stack
|
width: 56,
|
||||||
direction={'row'}
|
height: 56,
|
||||||
gap={'1rem'}
|
borderRadius: '12px',
|
||||||
alignItems={'center'}
|
}}
|
||||||
marginBottom={'1rem'}
|
src={icon}
|
||||||
>
|
/>
|
||||||
<Avatar
|
<Box>
|
||||||
variant='square'
|
<Typography variant="h5" fontWeight={600}>
|
||||||
sx={{
|
{appName}
|
||||||
width: 56,
|
</Typography>
|
||||||
height: 56,
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
borderRadius: '12px',
|
Would like your permission to
|
||||||
}}
|
</Typography>
|
||||||
src={icon}
|
</Box>
|
||||||
/>
|
</Stack>
|
||||||
<Box>
|
|
||||||
<Typography variant='h5' fontWeight={600}>
|
|
||||||
{appName}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color={'GrayText'}>
|
|
||||||
Would like your permission to
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<StyledActionsListContainer marginBottom={'1rem'}>
|
<StyledActionsListContainer marginBottom={'1rem'}>
|
||||||
<SectionTitle>Actions</SectionTitle>
|
<SectionTitle>Actions</SectionTitle>
|
||||||
<List>
|
<List>
|
||||||
{pendingRequests.map((req) => {
|
{pendingRequests.map((req) => {
|
||||||
return (
|
return (
|
||||||
<ListItem key={req.id}>
|
<ListItem key={req.id}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Checkbox
|
<Checkbox checked={req.checked} onChange={handleChangeCheckbox(req.id)} />
|
||||||
checked={req.checked}
|
</ListItemIcon>
|
||||||
onChange={handleChangeCheckbox(
|
<ListItemText>{getAction(req)}</ListItemText>
|
||||||
req.id,
|
</ListItem>
|
||||||
)}
|
)
|
||||||
/>
|
})}
|
||||||
</ListItemIcon>
|
</List>
|
||||||
<ListItemText>
|
</StyledActionsListContainer>
|
||||||
{getAction(req)}
|
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||||||
</ListItemText>
|
<ActionToggleButton value={ACTION_TYPE.ALWAYS} title="Always" />
|
||||||
</ListItem>
|
<ActionToggleButton value={ACTION_TYPE.ONCE} title="Just once" />
|
||||||
)
|
{/* <ActionToggleButton
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</StyledActionsListContainer>
|
|
||||||
<StyledToggleButtonsGroup
|
|
||||||
value={selectedActionType}
|
|
||||||
onChange={handleActionTypeChange}
|
|
||||||
exclusive
|
|
||||||
>
|
|
||||||
<ActionToggleButton
|
|
||||||
value={ACTION_TYPE.ALWAYS}
|
|
||||||
title='Always'
|
|
||||||
/>
|
|
||||||
<ActionToggleButton
|
|
||||||
value={ACTION_TYPE.ONCE}
|
|
||||||
title='Just once'
|
|
||||||
/>
|
|
||||||
{/* <ActionToggleButton
|
|
||||||
value={ACTION_TYPE.ALLOW_ALL}
|
value={ACTION_TYPE.ALLOW_ALL}
|
||||||
title='Allow All Advanced Actions'
|
title='Allow All Advanced Actions'
|
||||||
hasinfo
|
hasinfo
|
||||||
/> */}
|
/> */}
|
||||||
</StyledToggleButtonsGroup>
|
</StyledToggleButtonsGroup>
|
||||||
|
|
||||||
<Stack direction={'row'} gap={'1rem'}>
|
<Stack direction={'row'} gap={'1rem'}>
|
||||||
<StyledButton
|
<StyledButton onClick={() => confirmPending(false)} varianttype="secondary">
|
||||||
onClick={() => confirmPending(false)}
|
Disallow {ACTION_LABELS[selectedActionType]}
|
||||||
varianttype='secondary'
|
</StyledButton>
|
||||||
>
|
<StyledButton onClick={() => confirmPending(true)}>Allow {ACTION_LABELS[selectedActionType]}</StyledButton>
|
||||||
Disallow {ACTION_LABELS[selectedActionType]}
|
</Stack>
|
||||||
</StyledButton>
|
</Stack>
|
||||||
<StyledButton onClick={() => confirmPending(true)}>
|
</Modal>
|
||||||
Allow {ACTION_LABELS[selectedActionType]}
|
)
|
||||||
</StyledButton>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,31 @@
|
|||||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||||
import {
|
import { Stack, StackProps, ToggleButtonGroup, ToggleButtonGroupProps, styled } from '@mui/material'
|
||||||
Stack,
|
|
||||||
StackProps,
|
|
||||||
ToggleButtonGroup,
|
|
||||||
ToggleButtonGroupProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export const StyledButton = styled((props: AppButtonProps) => (
|
export const StyledButton = styled((props: AppButtonProps) => <Button {...props} />)(() => ({
|
||||||
<Button {...props} />
|
borderRadius: '19px',
|
||||||
))(() => ({
|
fontWeight: 600,
|
||||||
borderRadius: '19px',
|
padding: '0.75rem 1rem',
|
||||||
fontWeight: 600,
|
maxHeight: '41px',
|
||||||
padding: '0.75rem 1rem',
|
|
||||||
maxHeight: '41px',
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const StyledToggleButtonsGroup = styled(
|
export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />)(
|
||||||
(props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />,
|
() => ({
|
||||||
)(() => ({
|
gap: '0.75rem',
|
||||||
gap: '0.75rem',
|
marginBottom: '1rem',
|
||||||
marginBottom: '1rem',
|
justifyContent: 'space-between',
|
||||||
justifyContent: 'space-between',
|
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': {
|
||||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)':
|
margin: '0',
|
||||||
{
|
border: 'initial',
|
||||||
margin: '0',
|
},
|
||||||
border: 'initial',
|
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': {
|
||||||
},
|
border: 'initial',
|
||||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': {
|
borderRadius: '1rem',
|
||||||
border: 'initial',
|
},
|
||||||
borderRadius: '1rem',
|
})
|
||||||
},
|
)
|
||||||
}))
|
|
||||||
|
|
||||||
export const StyledActionsListContainer = styled((props: StackProps) => (
|
export const StyledActionsListContainer = styled((props: StackProps) => <Stack {...props} />)(({ theme }) => ({
|
||||||
<Stack {...props} />
|
padding: '0.75rem',
|
||||||
))(({ theme }) => ({
|
background: theme.palette.backgroundSecondary.default,
|
||||||
padding: '0.75rem',
|
borderRadius: '1rem',
|
||||||
background: theme.palette.backgroundSecondary.default,
|
|
||||||
borderRadius: '1rem',
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -3,22 +3,19 @@ import { ToggleButtonProps, Typography } from '@mui/material'
|
|||||||
import { StyledToggleButton } from './styled'
|
import { StyledToggleButton } from './styled'
|
||||||
|
|
||||||
type ActionToggleButtonProps = ToggleButtonProps & {
|
type ActionToggleButtonProps = ToggleButtonProps & {
|
||||||
hasinfo?: boolean
|
hasinfo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({
|
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
|
||||||
hasinfo = false,
|
const { title } = props
|
||||||
...props
|
return (
|
||||||
}) => {
|
<StyledToggleButton {...props}>
|
||||||
const { title } = props
|
<Typography variant="body2">{title}</Typography>
|
||||||
return (
|
{hasinfo && (
|
||||||
<StyledToggleButton {...props}>
|
<Typography className="info" color={'GrayText'}>
|
||||||
<Typography variant='body2'>{title}</Typography>
|
Info
|
||||||
{hasinfo && (
|
</Typography>
|
||||||
<Typography className='info' color={'GrayText'}>
|
)}
|
||||||
Info
|
</StyledToggleButton>
|
||||||
</Typography>
|
)
|
||||||
)}
|
|
||||||
</StyledToggleButton>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { ToggleButton, ToggleButtonProps, styled } from '@mui/material'
|
import { ToggleButton, ToggleButtonProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledToggleButton = styled((props: ToggleButtonProps) => (
|
export const StyledToggleButton = styled((props: ToggleButtonProps) => (
|
||||||
<ToggleButton classes={{ selected: 'selected' }} {...props} />
|
<ToggleButton classes={{ selected: 'selected' }} {...props} />
|
||||||
))(({ theme }) => ({
|
))(({ theme }) => ({
|
||||||
'&:is(&, :hover, :active)': {
|
'&:is(&, :hover, :active)': {
|
||||||
background: theme.palette.backgroundSecondary.default,
|
background: theme.palette.backgroundSecondary.default,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
flex: '1 0 6.25rem',
|
flex: '1 0 6.25rem',
|
||||||
height: '100px',
|
height: '100px',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
border: `2px solid transparent !important`,
|
border: `2px solid transparent !important`,
|
||||||
'&.selected': {
|
'&.selected': {
|
||||||
border: `2px solid ${theme.palette.text.primary} !important`,
|
border: `2px solid ${theme.palette.text.primary} !important`,
|
||||||
},
|
},
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
textTransform: 'initial',
|
textTransform: 'initial',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
'& .description': {
|
'& .description': {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
lineHeight: '15px',
|
lineHeight: '15px',
|
||||||
margin: '0.5rem 0 0.25rem',
|
margin: '0.5rem 0 0.25rem',
|
||||||
},
|
},
|
||||||
'& .info': {
|
'& .info': {
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -12,81 +12,64 @@ import { useRef } from 'react'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalConnectApp = () => {
|
export const ModalConnectApp = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||||
const timerRef = useRef<NodeJS.Timeout>()
|
const timerRef = useRef<NodeJS.Timeout>()
|
||||||
|
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
const handleCloseModal = createHandleCloseReplace(
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
|
||||||
MODAL_PARAMS_KEYS.CONNECT_APP,
|
onClose: () => {
|
||||||
{
|
clearTimeout(timerRef.current)
|
||||||
onClose: () => {
|
},
|
||||||
clearTimeout(timerRef.current)
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
|
||||||
const bunkerStr = getBunkerLink(npub)
|
const bunkerStr = getBunkerLink(npub)
|
||||||
|
|
||||||
const handleShareBunker = async () => {
|
const handleShareBunker = async () => {
|
||||||
const shareData = {
|
const shareData = {
|
||||||
text: bunkerStr,
|
text: bunkerStr,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (navigator.share && navigator.canShare(shareData)) {
|
if (navigator.share && navigator.canShare(shareData)) {
|
||||||
await navigator.share(shareData)
|
await navigator.share(shareData)
|
||||||
} else {
|
} else {
|
||||||
navigator.clipboard.writeText(bunkerStr)
|
navigator.clipboard.writeText(bunkerStr)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
notify('Your browser does not support sharing data', 'warning')
|
notify('Your browser does not support sharing data', 'warning')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
handleCloseModal()
|
handleCloseModal()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal open={isModalOpened} title="Share your profile" onClose={handleCloseModal}>
|
||||||
open={isModalOpened}
|
<Stack gap={'1rem'} alignItems={'center'}>
|
||||||
title='Share your profile'
|
<Typography variant="caption">Please, copy this code and paste it into the app to log in</Typography>
|
||||||
onClose={handleCloseModal}
|
<Input
|
||||||
>
|
sx={{
|
||||||
<Stack gap={'1rem'} alignItems={'center'}>
|
gap: '0.5rem',
|
||||||
<Typography variant='caption'>
|
}}
|
||||||
Please, copy this code and paste it into the app to log in
|
fullWidth
|
||||||
</Typography>
|
value={bunkerStr}
|
||||||
<Input
|
endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
|
||||||
sx={{
|
/>
|
||||||
gap: '0.5rem',
|
<AppLink title="What is this?" onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)} />
|
||||||
}}
|
<Button fullWidth onClick={handleShareBunker}>
|
||||||
fullWidth
|
Share it
|
||||||
value={bunkerStr}
|
</Button>
|
||||||
endAdornment={
|
<Button fullWidth onClick={handleCloseModal}>
|
||||||
<InputCopyButton
|
Done
|
||||||
value={bunkerStr}
|
</Button>
|
||||||
onCopy={handleCopy}
|
</Stack>
|
||||||
/>
|
</Modal>
|
||||||
}
|
)
|
||||||
/>
|
|
||||||
<AppLink
|
|
||||||
title='What is this?'
|
|
||||||
onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)}
|
|
||||||
/>
|
|
||||||
<Button fullWidth onClick={handleShareBunker}>
|
|
||||||
Share it
|
|
||||||
</Button>
|
|
||||||
<Button fullWidth onClick={handleCloseModal}>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,39 +7,37 @@ import { Button } from '@/shared/Button/Button'
|
|||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
type ModalExplanationProps = {
|
type ModalExplanationProps = {
|
||||||
explanationText?: string
|
explanationText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModalExplanation: FC<ModalExplanationProps> = ({
|
export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText = '' }) => {
|
||||||
explanationText = '',
|
const { getModalOpened } = useModalSearchParams()
|
||||||
}) => {
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||||
const { getModalOpened } = useModalSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
searchParams.delete('type')
|
searchParams.delete('type')
|
||||||
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
|
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||||
setSearchParams(searchParams)
|
setSearchParams(searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title='What is this?'
|
title="What is this?"
|
||||||
open={isModalOpened}
|
open={isModalOpened}
|
||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: {
|
||||||
minHeight: '60%',
|
minHeight: '60%',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack height={'100%'}>
|
<Stack height={'100%'}>
|
||||||
<Typography flex={1}>{explanationText}</Typography>
|
<Typography flex={1}>{explanationText}</Typography>
|
||||||
<Button fullWidth onClick={handleCloseModal}>
|
<Button fullWidth onClick={handleCloseModal}>
|
||||||
Got it!
|
Got it!
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,56 +11,51 @@ import { StyledAppLogo } from './styled'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalImportKeys = () => {
|
export const ModalImportKeys = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [enteredNsec, setEnteredNsec] = useState('')
|
const [enteredNsec, setEnteredNsec] = useState('')
|
||||||
|
|
||||||
const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setEnteredNsec(e.target.value)
|
setEnteredNsec(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
try {
|
try {
|
||||||
if (!enteredNsec.trim().length) return
|
if (!enteredNsec.trim().length) return
|
||||||
const enteredName = '' // FIXME get from input
|
const enteredName = '' // FIXME get from input
|
||||||
const k: any = await swicCall('importKey', enteredName, enteredNsec)
|
const k: any = await swicCall('importKey', enteredName, enteredNsec)
|
||||||
notify('Key imported!', 'success')
|
notify('Key imported!', 'success')
|
||||||
navigate(`/key/${k.npub}`)
|
navigate(`/key/${k.npub}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||||
<Stack
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||||
direction={'row'}
|
<StyledAppLogo />
|
||||||
gap={'1rem'}
|
<Typography fontWeight={600} variant="h5">
|
||||||
alignItems={'center'}
|
Import keys
|
||||||
alignSelf={'flex-start'}
|
</Typography>
|
||||||
>
|
</Stack>
|
||||||
<StyledAppLogo />
|
<Input
|
||||||
<Typography fontWeight={600} variant='h5'>
|
label="Enter a NSEC"
|
||||||
Import keys
|
placeholder="Your NSEC"
|
||||||
</Typography>
|
value={enteredNsec}
|
||||||
</Stack>
|
onChange={handleNsecChange}
|
||||||
<Input
|
fullWidth
|
||||||
label='Enter a NSEC'
|
type="password"
|
||||||
placeholder='Your NSEC'
|
/>
|
||||||
value={enteredNsec}
|
<Button type="submit">Import nsec</Button>
|
||||||
onChange={handleNsecChange}
|
</Stack>
|
||||||
fullWidth
|
</Modal>
|
||||||
type='password'
|
)
|
||||||
/>
|
|
||||||
<Button type='submit'>Import nsec</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { AppLogo } from '@/assets'
|
|||||||
import { Box, styled } from '@mui/material'
|
import { Box, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledAppLogo = styled((props) => (
|
export const StyledAppLogo = styled((props) => (
|
||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#00000054',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,52 +7,38 @@ import { Fade, Stack } from '@mui/material'
|
|||||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||||
|
|
||||||
export const ModalInitial = () => {
|
export const ModalInitial = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
||||||
|
|
||||||
const handleShowAdvanced = () => {
|
const handleShowAdvanced = () => {
|
||||||
setShowAdvancedContent(true)
|
setShowAdvancedContent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (isModalOpened) {
|
if (isModalOpened) {
|
||||||
setShowAdvancedContent(false)
|
setShowAdvancedContent(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isModalOpened])
|
}, [isModalOpened])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
||||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>Sign up</Button>
|
||||||
Sign up
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>Login</Button>
|
||||||
</Button>
|
<AppLink title="Advanced" alignSelf={'center'} onClick={handleShowAdvanced} />
|
||||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
<AppLink
|
|
||||||
title='Advanced'
|
|
||||||
alignSelf={'center'}
|
|
||||||
onClick={handleShowAdvanced}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showAdvancedContent && (
|
{showAdvancedContent && (
|
||||||
<Fade in>
|
<Fade in>
|
||||||
<Button
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import keys</Button>
|
||||||
onClick={() =>
|
</Fade>
|
||||||
handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
)}
|
||||||
}
|
</Stack>
|
||||||
>
|
</Modal>
|
||||||
Import keys
|
)
|
||||||
</Button>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,128 +18,110 @@ import { DOMAIN } from '@/utils/consts'
|
|||||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
|
|
||||||
export const ModalLogin = () => {
|
export const ModalLogin = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormInputType>({
|
} = useForm<FormInputType>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
})
|
})
|
||||||
|
|
||||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||||
|
|
||||||
const handlePasswordTypeChange = () =>
|
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
|
||||||
setIsPasswordShown((prevState) => !prevState)
|
|
||||||
|
|
||||||
const cleanUpStates = useCallback(() => {
|
const cleanUpStates = useCallback(() => {
|
||||||
setIsPasswordShown(false)
|
setIsPasswordShown(false)
|
||||||
reset()
|
reset()
|
||||||
}, [reset])
|
}, [reset])
|
||||||
|
|
||||||
const submitHandler = async (values: FormInputType) => {
|
const submitHandler = async (values: FormInputType) => {
|
||||||
try {
|
try {
|
||||||
let npub = values.username
|
let npub = values.username
|
||||||
let name = ''
|
let name = ''
|
||||||
if (!npub.startsWith('npub1')) {
|
if (!npub.startsWith('npub1')) {
|
||||||
name = npub
|
name = npub
|
||||||
if (!npub.includes('@')) {
|
if (!npub.includes('@')) {
|
||||||
npub += '@' + DOMAIN
|
npub += '@' + DOMAIN
|
||||||
} else {
|
} else {
|
||||||
const nameDomain = npub.split('@')
|
const nameDomain = npub.split('@')
|
||||||
if (nameDomain[1] === DOMAIN)
|
if (nameDomain[1] === DOMAIN) name = nameDomain[0]
|
||||||
name = nameDomain[0];
|
}
|
||||||
}
|
}
|
||||||
}
|
if (npub.includes('@')) {
|
||||||
if (npub.includes('@')) {
|
const npubNip05 = await fetchNip05(npub)
|
||||||
const npubNip05 = await fetchNip05(npub)
|
if (!npubNip05) throw new Error(`Username ${npub} not found`)
|
||||||
if (!npubNip05) throw new Error(`Username ${npub} not found`)
|
npub = npubNip05
|
||||||
npub = npubNip05
|
}
|
||||||
}
|
const passphrase = values.password
|
||||||
const passphrase = values.password
|
|
||||||
|
|
||||||
console.log('fetch', npub, name)
|
console.log('fetch', npub, name)
|
||||||
const k: any = await swicCall('fetchKey', npub, passphrase, name)
|
const k: any = await swicCall('fetchKey', npub, passphrase, name)
|
||||||
notify(`Fetched ${k.npub}`, 'success')
|
notify(`Fetched ${k.npub}`, 'success')
|
||||||
cleanUpStates()
|
cleanUpStates()
|
||||||
navigate(`/key/${k.npub}`)
|
navigate(`/key/${k.npub}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log("error", error);
|
console.log('error', error)
|
||||||
notify(error?.message || 'Something went wrong!', 'error')
|
notify(error?.message || 'Something went wrong!', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (isModalOpened) {
|
if (isModalOpened) {
|
||||||
// modal closed
|
// modal closed
|
||||||
cleanUpStates()
|
cleanUpStates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isModalOpened, cleanUpStates])
|
}, [isModalOpened, cleanUpStates])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack
|
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||||
gap={'1rem'}
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||||
component={'form'}
|
<StyledAppLogo />
|
||||||
onSubmit={handleSubmit(submitHandler)}
|
<Typography fontWeight={600} variant="h5">
|
||||||
>
|
Login
|
||||||
<Stack
|
</Typography>
|
||||||
direction={'row'}
|
</Stack>
|
||||||
gap={'1rem'}
|
<Input
|
||||||
alignItems={'center'}
|
label="Username or nip05 or npub"
|
||||||
alignSelf={'flex-start'}
|
fullWidth
|
||||||
>
|
placeholder="name or name@domain.com or npub1..."
|
||||||
<StyledAppLogo />
|
{...register('username')}
|
||||||
<Typography fontWeight={600} variant='h5'>
|
error={!!errors.username}
|
||||||
Login
|
/>
|
||||||
</Typography>
|
<Input
|
||||||
</Stack>
|
label="Password"
|
||||||
<Input
|
fullWidth
|
||||||
label='Username or nip05 or npub'
|
placeholder="Your password"
|
||||||
fullWidth
|
{...register('password')}
|
||||||
placeholder='name or name@domain.com or npub1...'
|
endAdornment={
|
||||||
{...register('username')}
|
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||||
error={!!errors.username}
|
{isPasswordShown ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlinedIcon />}
|
||||||
/>
|
</IconButton>
|
||||||
<Input
|
}
|
||||||
label='Password'
|
type={isPasswordShown ? 'text' : 'password'}
|
||||||
fullWidth
|
error={!!errors.password}
|
||||||
placeholder='Your password'
|
/>
|
||||||
{...register('password')}
|
<Button type="submit" fullWidth>
|
||||||
endAdornment={
|
Add account
|
||||||
<IconButton
|
</Button>
|
||||||
size='small'
|
</Stack>
|
||||||
onClick={handlePasswordTypeChange}
|
</Modal>
|
||||||
>
|
)
|
||||||
{isPasswordShown ? (
|
|
||||||
<VisibilityOffOutlinedIcon />
|
|
||||||
) : (
|
|
||||||
<VisibilityOutlinedIcon />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
type={isPasswordShown ? 'text' : 'password'}
|
|
||||||
error={!!errors.password}
|
|
||||||
/>
|
|
||||||
<Button type='submit' fullWidth>
|
|
||||||
Add account
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import * as yup from 'yup'
|
import * as yup from 'yup'
|
||||||
|
|
||||||
export const schema = yup.object().shape({
|
export const schema = yup.object().shape({
|
||||||
username: yup
|
username: yup
|
||||||
.string()
|
.string()
|
||||||
.test('Domain validation', 'The domain is required!', function (value) {
|
.test('Domain validation', 'The domain is required!', function (value) {
|
||||||
if (!value || !value.trim().length) return false
|
if (!value || !value.trim().length) return false
|
||||||
|
|
||||||
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(
|
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g)
|
||||||
/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g,
|
return USERNAME_WITH_DOMAIN_REGEXP.test(value)
|
||||||
)
|
})
|
||||||
return USERNAME_WITH_DOMAIN_REGEXP.test(value)
|
.required(),
|
||||||
})
|
password: yup.string().required().min(4),
|
||||||
.required(),
|
|
||||||
password: yup.string().required().min(4),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type FormInputType = yup.InferType<typeof schema>
|
export type FormInputType = yup.InferType<typeof schema>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { AppLogo } from '@/assets'
|
|||||||
import { Box, styled } from '@mui/material'
|
import { Box, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledAppLogo = styled((props) => (
|
export const StyledAppLogo = styled((props) => (
|
||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#00000054',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,18 +2,8 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
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 {
|
import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material'
|
||||||
Box,
|
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material'
|
|
||||||
import {
|
|
||||||
StyledButton,
|
|
||||||
StyledSettingContainer,
|
|
||||||
StyledSynchedText,
|
|
||||||
} from './styled'
|
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { CheckmarkIcon } from '@/assets'
|
import { CheckmarkIcon } from '@/assets'
|
||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
@@ -27,144 +17,123 @@ import { useParams } from 'react-router-dom'
|
|||||||
import { dbi } from '@/modules/db'
|
import { dbi } from '@/modules/db'
|
||||||
|
|
||||||
type ModalSettingsProps = {
|
type ModalSettingsProps = {
|
||||||
isSynced: boolean
|
isSynced: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
|
||||||
const [enteredPassword, setEnteredPassword] = useState('')
|
const [enteredPassword, setEnteredPassword] = useState('')
|
||||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||||
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
const [isChecked, setIsChecked] = useState(false)
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
||||||
|
|
||||||
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsPasswordInvalid(false)
|
||||||
|
setEnteredPassword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
|
||||||
setIsPasswordInvalid(false)
|
|
||||||
setEnteredPassword(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePasswordTypeChange = () =>
|
const onClose = () => {
|
||||||
setIsPasswordShown((prevState) => !prevState)
|
handleCloseModal()
|
||||||
|
setEnteredPassword('')
|
||||||
|
setIsPasswordInvalid(false)
|
||||||
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
|
||||||
handleCloseModal()
|
setIsChecked(checked)
|
||||||
setEnteredPassword('')
|
}
|
||||||
setIsPasswordInvalid(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
setIsChecked(checked)
|
e.preventDefault()
|
||||||
}
|
setIsPasswordInvalid(false)
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
if (enteredPassword.trim().length < 6) {
|
||||||
e.preventDefault()
|
return setIsPasswordInvalid(true)
|
||||||
setIsPasswordInvalid(false)
|
}
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
await swicCall('saveKey', npub, enteredPassword)
|
||||||
|
notify('Key saved', 'success')
|
||||||
|
dbi.addSynced(npub) // Sync npub
|
||||||
|
setEnteredPassword('')
|
||||||
|
setIsPasswordInvalid(false)
|
||||||
|
setIsLoading(false)
|
||||||
|
} catch (error) {
|
||||||
|
setIsPasswordInvalid(false)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (enteredPassword.trim().length < 6) {
|
return (
|
||||||
return setIsPasswordInvalid(true)
|
<Modal open={isModalOpened} onClose={onClose} title="Settings">
|
||||||
}
|
<Stack gap={'1rem'}>
|
||||||
try {
|
<StyledSettingContainer onSubmit={handleSubmit}>
|
||||||
setIsLoading(true)
|
<Stack direction={'row'} justifyContent={'space-between'}>
|
||||||
await swicCall('saveKey', npub, enteredPassword)
|
<SectionTitle>Cloud sync</SectionTitle>
|
||||||
notify('Key saved', 'success')
|
{isSynced && (
|
||||||
dbi.addSynced(npub) // Sync npub
|
<StyledSynchedText>
|
||||||
setEnteredPassword('')
|
<CheckmarkIcon /> Synched
|
||||||
setIsPasswordInvalid(false)
|
</StyledSynchedText>
|
||||||
setIsLoading(false)
|
)}
|
||||||
} catch (error) {
|
</Stack>
|
||||||
setIsPasswordInvalid(false)
|
<Box>
|
||||||
setIsLoading(false)
|
<Checkbox onChange={handleChangeCheckbox} checked={isChecked} />
|
||||||
}
|
<Typography variant="caption">Use this key on multiple devices</Typography>
|
||||||
}
|
</Box>
|
||||||
|
<Input
|
||||||
return (
|
fullWidth
|
||||||
<Modal open={isModalOpened} onClose={onClose} title='Settings'>
|
endAdornment={
|
||||||
<Stack gap={'1rem'}>
|
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||||
<StyledSettingContainer onSubmit={handleSubmit}>
|
{isPasswordShown ? (
|
||||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
<VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
|
||||||
<SectionTitle>Cloud sync</SectionTitle>
|
) : (
|
||||||
{isSynced && (
|
<VisibilityOutlinedIcon htmlColor="#6b6b6b" />
|
||||||
<StyledSynchedText>
|
)}
|
||||||
<CheckmarkIcon /> Synched
|
</IconButton>
|
||||||
</StyledSynchedText>
|
}
|
||||||
)}
|
type={isPasswordShown ? 'text' : 'password'}
|
||||||
</Stack>
|
onChange={handlePasswordChange}
|
||||||
<Box>
|
value={enteredPassword}
|
||||||
<Checkbox
|
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
||||||
onChange={handleChangeCheckbox}
|
placeholder="Enter a password"
|
||||||
checked={isChecked}
|
helperTextProps={{
|
||||||
/>
|
sx: {
|
||||||
<Typography variant='caption'>
|
'&.helper_text': {
|
||||||
Use this key on multiple devices
|
color: 'red',
|
||||||
</Typography>
|
},
|
||||||
</Box>
|
},
|
||||||
<Input
|
}}
|
||||||
fullWidth
|
disabled={!isChecked}
|
||||||
endAdornment={
|
/>
|
||||||
<IconButton
|
{isSynced ? (
|
||||||
size='small'
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
onClick={handlePasswordTypeChange}
|
To change your password, type a new one and sync.
|
||||||
>
|
</Typography>
|
||||||
{isPasswordShown ? (
|
) : (
|
||||||
<VisibilityOffOutlinedIcon htmlColor='#6b6b6b' />
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
) : (
|
This key will be encrypted and stored on our server. You can use the password to download this key onto
|
||||||
<VisibilityOutlinedIcon htmlColor='#6b6b6b' />
|
another device.
|
||||||
)}
|
</Typography>
|
||||||
</IconButton>
|
)}
|
||||||
}
|
<StyledButton type="submit" fullWidth disabled={!isChecked}>
|
||||||
type={isPasswordShown ? 'text' : 'password'}
|
Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||||
onChange={handlePasswordChange}
|
</StyledButton>
|
||||||
value={enteredPassword}
|
</StyledSettingContainer>
|
||||||
helperText={
|
<Button onClick={onClose}>Done</Button>
|
||||||
isPasswordInvalid ? 'Invalid password' : ''
|
</Stack>
|
||||||
}
|
</Modal>
|
||||||
placeholder='Enter a password'
|
)
|
||||||
helperTextProps={{
|
|
||||||
sx: {
|
|
||||||
'&.helper_text': {
|
|
||||||
color: 'red',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disabled={!isChecked}
|
|
||||||
/>
|
|
||||||
{isSynced ? (
|
|
||||||
<Typography variant='body2' color={'GrayText'}>
|
|
||||||
To change your password, type a new one and sync.
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Typography variant='body2' color={'GrayText'}>
|
|
||||||
This key will be encrypted and stored on our server. You can use the password to download this key onto another device.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<StyledButton
|
|
||||||
type='submit'
|
|
||||||
fullWidth
|
|
||||||
disabled={!isChecked}
|
|
||||||
>
|
|
||||||
Sync{' '}
|
|
||||||
{isLoading && (
|
|
||||||
<CircularProgress
|
|
||||||
sx={{ marginLeft: '0.5rem' }}
|
|
||||||
size={'1rem'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledButton>
|
|
||||||
</StyledSettingContainer>
|
|
||||||
<Button onClick={onClose}>Done</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,31 @@
|
|||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import {
|
import { Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||||
Stack,
|
|
||||||
StackProps,
|
|
||||||
Typography,
|
|
||||||
TypographyProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||||
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||||
))(({ theme }) => ({
|
))(({ theme }) => ({
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const StyledButton = styled(Button)(({ theme }) => {
|
export const StyledButton = styled(Button)(({ theme }) => {
|
||||||
return {
|
return {
|
||||||
'&.button:is(:hover, :active, &)': {
|
'&.button:is(:hover, :active, &)': {
|
||||||
background: theme.palette.secondary.main,
|
background: theme.palette.secondary.main,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
':disabled': {
|
':disabled': {
|
||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledSynchedText = styled((props: TypographyProps) => (
|
export const StyledSynchedText = styled((props: TypographyProps) => <Typography variant="caption" {...props} />)(({
|
||||||
<Typography variant='caption' {...props} />
|
theme,
|
||||||
))(({ theme }) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
color: theme.palette.success.main,
|
color: theme.palette.success.main,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,103 +14,87 @@ import { DOMAIN, NOAUTHD_URL } from '@/utils/consts'
|
|||||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
|
|
||||||
export const ModalSignUp = () => {
|
export const ModalSignUp = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP)
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [enteredValue, setEnteredValue] = useState('')
|
const [enteredValue, setEnteredValue] = useState('')
|
||||||
const [isAvailable, setIsAvailable] = useState(false)
|
const [isAvailable, setIsAvailable] = useState(false)
|
||||||
|
|
||||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setEnteredValue(e.target.value)
|
setEnteredValue(e.target.value)
|
||||||
const name = e.target.value.trim()
|
const name = e.target.value.trim()
|
||||||
if (name) {
|
if (name) {
|
||||||
const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
|
const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
|
||||||
setIsAvailable(!npubNip05)
|
setIsAvailable(!npubNip05)
|
||||||
} else {
|
} else {
|
||||||
setIsAvailable(false)
|
setIsAvailable(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputHelperText = enteredValue
|
const inputHelperText = enteredValue ? (
|
||||||
? (
|
isAvailable ? (
|
||||||
isAvailable ? (
|
<>
|
||||||
<>
|
<CheckmarkIcon /> Available
|
||||||
<CheckmarkIcon /> Available
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<>Already taken</>
|
||||||
<>
|
)
|
||||||
Already taken
|
) : (
|
||||||
</>
|
"Don't worry, username can be changed later."
|
||||||
)
|
)
|
||||||
) : (
|
|
||||||
"Don't worry, username can be changed later."
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
const name = enteredValue.trim()
|
const name = enteredValue.trim()
|
||||||
if (!name.length) return
|
if (!name.length) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
try {
|
try {
|
||||||
const k: any = await swicCall('generateKey', name)
|
const k: any = await swicCall('generateKey', name)
|
||||||
notify(`Account created for "${name}"`, 'success')
|
notify(`Account created for "${name}"`, 'success')
|
||||||
navigate(`/key/${k.npub}`)
|
navigate(`/key/${k.npub}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack
|
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||||
paddingTop={'1rem'}
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||||
gap={'1rem'}
|
<StyledAppLogo />
|
||||||
component={'form'}
|
<Typography fontWeight={600} variant="h5">
|
||||||
onSubmit={handleSubmit}
|
Sign up
|
||||||
>
|
</Typography>
|
||||||
<Stack
|
</Stack>
|
||||||
direction={'row'}
|
<Input
|
||||||
gap={'1rem'}
|
label="Enter a Username"
|
||||||
alignItems={'center'}
|
fullWidth
|
||||||
alignSelf={'flex-start'}
|
placeholder="Username"
|
||||||
>
|
helperText={inputHelperText}
|
||||||
<StyledAppLogo />
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
<Typography fontWeight={600} variant='h5'>
|
onChange={handleInputChange}
|
||||||
Sign up
|
value={enteredValue}
|
||||||
</Typography>
|
helperTextProps={{
|
||||||
</Stack>
|
sx: {
|
||||||
<Input
|
'&.helper_text': {
|
||||||
label='Enter a Username'
|
color:
|
||||||
fullWidth
|
enteredValue && isAvailable
|
||||||
placeholder='Username'
|
? theme.palette.success.main
|
||||||
helperText={inputHelperText}
|
: enteredValue && !isAvailable
|
||||||
endAdornment={
|
? theme.palette.error.main
|
||||||
<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>
|
: theme.palette.textSecondaryDecorate.main,
|
||||||
}
|
},
|
||||||
onChange={handleInputChange}
|
},
|
||||||
value={enteredValue}
|
}}
|
||||||
helperTextProps={{
|
/>
|
||||||
sx: {
|
<Button fullWidth type="submit">
|
||||||
'&.helper_text': {
|
Create account
|
||||||
color: enteredValue && isAvailable
|
</Button>
|
||||||
? theme.palette.success.main
|
</Stack>
|
||||||
: (enteredValue && !isAvailable
|
</Modal>
|
||||||
? theme.palette.error.main
|
)
|
||||||
: theme.palette.textSecondaryDecorate.main
|
|
||||||
)
|
|
||||||
,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button fullWidth type='submit'>
|
|
||||||
Create account
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { AppLogo } from '@/assets'
|
|||||||
import { Box, styled } from '@mui/material'
|
import { Box, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledAppLogo = styled((props) => (
|
export const StyledAppLogo = styled((props) => (
|
||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#00000054',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,21 +5,19 @@ import CloseIcon from '@mui/icons-material/Close'
|
|||||||
import { NotificationProps } from './types'
|
import { NotificationProps } from './types'
|
||||||
import { StyledAlert, StyledContainer } from './styled'
|
import { StyledAlert, StyledContainer } from './styled'
|
||||||
|
|
||||||
export const Notification = forwardRef<HTMLDivElement, NotificationProps>(
|
export const Notification = forwardRef<HTMLDivElement, NotificationProps>(({ message, alertvariant, id }, ref) => {
|
||||||
({ message, alertvariant, id }, ref) => {
|
const { closeSnackbar } = useSnackbar()
|
||||||
const { closeSnackbar } = useSnackbar()
|
|
||||||
|
|
||||||
const closeSnackBarHandler = () => closeSnackbar(id)
|
const closeSnackBarHandler = () => closeSnackbar(id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAlert alertvariant={alertvariant} ref={ref}>
|
<StyledAlert alertvariant={alertvariant} ref={ref}>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Typography variant='body1'>{message}</Typography>
|
<Typography variant="body1">{message}</Typography>
|
||||||
<IconButton onClick={closeSnackBarHandler} color='inherit'>
|
<IconButton onClick={closeSnackBarHandler} color="inherit">
|
||||||
<CloseIcon color='inherit' />
|
<CloseIcon color="inherit" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
</StyledAlert>
|
</StyledAlert>
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { VariantType } from 'notistack'
|
|||||||
type Variant = Exclude<VariantType, 'default' | 'info'>
|
type Variant = Exclude<VariantType, 'default' | 'info'>
|
||||||
|
|
||||||
export const BORDER_STYLES: Record<Variant, string> = {
|
export const BORDER_STYLES: Record<Variant, string> = {
|
||||||
error: '#b90e0a',
|
error: '#b90e0a',
|
||||||
success: '#32cd32',
|
success: '#32cd32',
|
||||||
warning: '#FF9500',
|
warning: '#FF9500',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,43 +4,41 @@ import { BORDER_STYLES } from './const'
|
|||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
export const StyledAlert = styled(
|
export const StyledAlert = styled(
|
||||||
forwardRef<HTMLDivElement, StyledAlertProps>((props, ref) => (
|
forwardRef<HTMLDivElement, StyledAlertProps>((props, ref) => <Alert {...props} ref={ref} icon={false} />)
|
||||||
<Alert {...props} ref={ref} icon={false} />
|
|
||||||
)),
|
|
||||||
)(({ alertvariant }) => ({
|
)(({ alertvariant }) => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxHeight: 56,
|
maxHeight: 56,
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1rem',
|
||||||
backgroundColor: '#FFF',
|
backgroundColor: '#FFF',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
border: `solid ${BORDER_STYLES[alertvariant]} 1px`,
|
border: `solid ${BORDER_STYLES[alertvariant]} 1px`,
|
||||||
color: BORDER_STYLES[alertvariant],
|
color: BORDER_STYLES[alertvariant],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
'& .MuiAlert-message': {
|
'& .MuiAlert-message': {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
minWidth: '100%',
|
minWidth: '100%',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const StyledContainer = styled(Box)(() => ({
|
export const StyledContainer = styled(Box)(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
'& > .MuiTypography-root': {
|
'& > .MuiTypography-root': {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { AlertProps } from '@mui/material'
|
|||||||
import { SnackbarKey, VariantType } from 'notistack'
|
import { SnackbarKey, VariantType } from 'notistack'
|
||||||
|
|
||||||
export type StyledAlertProps = Omit<AlertProps, 'id'> & {
|
export type StyledAlertProps = Omit<AlertProps, 'id'> & {
|
||||||
alertvariant: Exclude<VariantType, 'default' | 'info'>
|
alertvariant: Exclude<VariantType, 'default' | 'info'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotificationProps = {
|
export type NotificationProps = {
|
||||||
message: string
|
message: string
|
||||||
id: SnackbarKey
|
id: SnackbarKey
|
||||||
} & StyledAlertProps
|
} & StyledAlertProps
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import { IconContainer, StyledContainer } from './styled'
|
|||||||
import { BoxProps, Typography } from '@mui/material'
|
import { BoxProps, Typography } from '@mui/material'
|
||||||
|
|
||||||
type WarningProps = {
|
type WarningProps = {
|
||||||
message: string | ReactNode
|
message: string | ReactNode
|
||||||
Icon?: ReactNode
|
Icon?: ReactNode
|
||||||
} & BoxProps
|
} & BoxProps
|
||||||
|
|
||||||
export const Warning: FC<WarningProps> = ({ message, Icon, ...restProps }) => {
|
export const Warning: FC<WarningProps> = ({ message, Icon, ...restProps }) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer {...restProps}>
|
<StyledContainer {...restProps}>
|
||||||
{Icon && <IconContainer>{Icon}</IconContainer>}
|
{Icon && <IconContainer>{Icon}</IconContainer>}
|
||||||
<Typography flex={1} noWrap>
|
<Typography flex={1} noWrap>
|
||||||
{message}
|
{message}
|
||||||
</Typography>
|
</Typography>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
import { Box, BoxProps, styled } from '@mui/material'
|
import { Box, BoxProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)(
|
export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)(() => {
|
||||||
() => {
|
return {
|
||||||
return {
|
borderRadius: '4px',
|
||||||
borderRadius: '4px',
|
border: '1px solid grey',
|
||||||
border: '1px solid grey',
|
padding: '0.5rem',
|
||||||
padding: '0.5rem',
|
display: 'flex',
|
||||||
display: 'flex',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
gap: '1rem',
|
||||||
gap: '1rem',
|
cursor: 'pointer',
|
||||||
cursor: 'pointer',
|
}
|
||||||
}
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(
|
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||||
() => ({
|
width: '40px',
|
||||||
width: '40px',
|
height: '40px',
|
||||||
height: '40px',
|
borderRadius: '50%',
|
||||||
borderRadius: '50%',
|
background: 'blue',
|
||||||
background: 'blue',
|
display: 'grid',
|
||||||
display: 'grid',
|
placeItems: 'center',
|
||||||
placeItems: 'center',
|
}))
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
import {
|
import { useSnackbar as useDefaultSnackbar, OptionsObject, VariantType } from 'notistack'
|
||||||
useSnackbar as useDefaultSnackbar,
|
|
||||||
OptionsObject,
|
|
||||||
VariantType,
|
|
||||||
} from 'notistack'
|
|
||||||
import { Notification } from '../components/Notification/Notification'
|
import { Notification } from '../components/Notification/Notification'
|
||||||
|
|
||||||
export const useEnqueueSnackbar = () => {
|
export const useEnqueueSnackbar = () => {
|
||||||
const { enqueueSnackbar } = useDefaultSnackbar()
|
const { enqueueSnackbar } = useDefaultSnackbar()
|
||||||
|
|
||||||
const showSnackbar = (
|
const showSnackbar = (message: string, variant: Exclude<VariantType, 'default' | 'info'> = 'success') => {
|
||||||
message: string,
|
enqueueSnackbar(message, {
|
||||||
variant: Exclude<VariantType, 'default' | 'info'> = 'success',
|
anchorOrigin: {
|
||||||
) => {
|
vertical: 'top',
|
||||||
enqueueSnackbar(message, {
|
horizontal: 'right',
|
||||||
anchorOrigin: {
|
},
|
||||||
vertical: 'top',
|
content: (id) => {
|
||||||
horizontal: 'right',
|
return <Notification id={id} message={message} alertvariant={variant} />
|
||||||
},
|
},
|
||||||
content: (id) => {
|
} as OptionsObject)
|
||||||
return (
|
}
|
||||||
<Notification
|
|
||||||
id={id}
|
|
||||||
message={message}
|
|
||||||
alertvariant={variant}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
} as OptionsObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return showSnackbar
|
return showSnackbar
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ import { useState, useEffect } from 'react'
|
|||||||
const iOSRegex = /iPad|iPhone|iPod/
|
const iOSRegex = /iPad|iPhone|iPod/
|
||||||
|
|
||||||
function useIsIOS() {
|
function useIsIOS() {
|
||||||
const [isIOS, setIsIOS] = useState(false)
|
const [isIOS, setIsIOS] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isIOSUserAgent =
|
const isIOSUserAgent =
|
||||||
iOSRegex.test(navigator.userAgent) ||
|
iOSRegex.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
||||||
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
setIsIOS(isIOSUserAgent)
|
||||||
setIsIOS(isIOSUserAgent)
|
}, [])
|
||||||
}, [])
|
|
||||||
|
|
||||||
return isIOS
|
return isIOS
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useIsIOS
|
export default useIsIOS
|
||||||
|
|||||||
@@ -1,96 +1,84 @@
|
|||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import {
|
import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
createSearchParams,
|
|
||||||
useLocation,
|
|
||||||
useNavigate,
|
|
||||||
useSearchParams,
|
|
||||||
} from 'react-router-dom'
|
|
||||||
|
|
||||||
type SearchParamsType = {
|
type SearchParamsType = {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IExtraOptions = {
|
export type IExtraOptions = {
|
||||||
search?: SearchParamsType
|
search?: SearchParamsType
|
||||||
replace?: boolean
|
replace?: boolean
|
||||||
append?: boolean
|
append?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IExtraCloseOptions = {
|
export type IExtraCloseOptions = {
|
||||||
replace?: boolean
|
replace?: boolean
|
||||||
onClose?: (s: URLSearchParams) => void
|
onClose?: (s: URLSearchParams) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useModalSearchParams = () => {
|
export const useModalSearchParams = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const getEnumParam = useCallback((modal: MODAL_PARAMS_KEYS) => {
|
const getEnumParam = useCallback((modal: MODAL_PARAMS_KEYS) => {
|
||||||
return Object.values(MODAL_PARAMS_KEYS)[
|
return Object.values(MODAL_PARAMS_KEYS)[Object.values(MODAL_PARAMS_KEYS).indexOf(modal)]
|
||||||
Object.values(MODAL_PARAMS_KEYS).indexOf(modal)
|
}, [])
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const createHandleClose =
|
const createHandleClose = (modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) => () => {
|
||||||
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) =>
|
const enumKey = getEnumParam(modal)
|
||||||
() => {
|
searchParams.delete(enumKey)
|
||||||
const enumKey = getEnumParam(modal)
|
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
||||||
searchParams.delete(enumKey)
|
console.log({ searchParams })
|
||||||
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
||||||
console.log({ searchParams })
|
}
|
||||||
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
|
||||||
}
|
|
||||||
|
|
||||||
const createHandleCloseReplace =
|
const createHandleCloseReplace = (modal: MODAL_PARAMS_KEYS, extraOptions: IExtraCloseOptions = {}) => {
|
||||||
(modal: MODAL_PARAMS_KEYS, extraOptions: IExtraCloseOptions = {}) => {
|
return createHandleClose(modal, { ...extraOptions, replace: true })
|
||||||
return createHandleClose(modal, { ...extraOptions, replace: true })
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpen = useCallback(
|
const handleOpen = useCallback(
|
||||||
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraOptions) => {
|
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraOptions) => {
|
||||||
const enumKey = getEnumParam(modal)
|
const enumKey = getEnumParam(modal)
|
||||||
|
|
||||||
let searchParamsData: SearchParamsType = { [enumKey]: 'true' }
|
let searchParamsData: SearchParamsType = { [enumKey]: 'true' }
|
||||||
if (extraOptions?.search) {
|
if (extraOptions?.search) {
|
||||||
searchParamsData = {
|
searchParamsData = {
|
||||||
...searchParamsData,
|
...searchParamsData,
|
||||||
...extraOptions.search,
|
...extraOptions.search,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchString = !extraOptions?.append
|
const searchString = !extraOptions?.append
|
||||||
? createSearchParams(searchParamsData).toString()
|
? createSearchParams(searchParamsData).toString()
|
||||||
: `${location.search}&${createSearchParams(
|
: `${location.search}&${createSearchParams(searchParamsData).toString()}`
|
||||||
searchParamsData,
|
|
||||||
).toString()}`
|
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
{
|
{
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: searchString,
|
search: searchString,
|
||||||
},
|
},
|
||||||
{ replace: !!extraOptions?.replace },
|
{ replace: !!extraOptions?.replace }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[location, navigate, getEnumParam],
|
[location, navigate, getEnumParam]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getModalOpened = useCallback(
|
const getModalOpened = useCallback(
|
||||||
(modal: MODAL_PARAMS_KEYS) => {
|
(modal: MODAL_PARAMS_KEYS) => {
|
||||||
const enumKey = getEnumParam(modal)
|
const enumKey = getEnumParam(modal)
|
||||||
const modalOpened = searchParams.get(enumKey) === 'true'
|
const modalOpened = searchParams.get(enumKey) === 'true'
|
||||||
return modalOpened
|
return modalOpened
|
||||||
},
|
},
|
||||||
[getEnumParam, searchParams],
|
[getEnumParam, searchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getModalOpened,
|
getModalOpened,
|
||||||
createHandleClose,
|
createHandleClose,
|
||||||
createHandleCloseReplace,
|
createHandleCloseReplace,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
export const useOpenMenu = () => {
|
export const useOpenMenu = () => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||||
|
|
||||||
const open = Boolean(anchorEl)
|
const open = Boolean(anchorEl)
|
||||||
|
|
||||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget)
|
setAnchorEl(event.currentTarget)
|
||||||
}
|
}
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null)
|
setAnchorEl(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open,
|
open,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
anchorEl,
|
anchorEl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
export const useToggleConfirm = () => {
|
export const useToggleConfirm = () => {
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
|
||||||
const handleShow = useCallback(() => setShowConfirm(true), [])
|
const handleShow = useCallback(() => setShowConfirm(true), [])
|
||||||
|
|
||||||
const handleClose = useCallback(() => setShowConfirm(false), [])
|
const handleClose = useCallback(() => setShowConfirm(false), [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open: showConfirm,
|
open: showConfirm,
|
||||||
handleShow,
|
handleShow,
|
||||||
handleClose,
|
handleClose,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
||||||
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
'Helvetica Neue', sans-serif;
|
-webkit-font-smoothing: antialiased;
|
||||||
-webkit-font-smoothing: antialiased;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,19 @@ import { SnackbarProvider } from 'notistack'
|
|||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SnackbarProvider maxSnack={3} autoHideDuration={3000}>
|
<SnackbarProvider maxSnack={3} autoHideDuration={3000}>
|
||||||
<App />
|
<App />
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
|||||||
@@ -10,57 +10,47 @@ import { ProfileMenu } from './components/ProfileMenu'
|
|||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!npub) return setProfile(null)
|
if (!npub) return setProfile(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchProfile(npub)
|
const response = await fetchProfile(npub)
|
||||||
setProfile(response as any)
|
setProfile(response as any)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return setProfile(null)
|
return setProfile(null)
|
||||||
}
|
}
|
||||||
}, [npub])
|
}, [npub])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
const showProfile = Boolean(npub || profile)
|
const showProfile = Boolean(npub || profile)
|
||||||
const userName = profile?.info?.name || getShortenNpub(npub)
|
const userName = profile?.info?.name || getShortenNpub(npub)
|
||||||
const userAvatar = profile?.info?.picture || ''
|
const userAvatar = profile?.info?.picture || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAppBar position='fixed'>
|
<StyledAppBar position="fixed">
|
||||||
<Toolbar sx={{ padding: '12px' }}>
|
<Toolbar sx={{ padding: '12px' }}>
|
||||||
<Stack
|
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
||||||
direction={'row'}
|
{showProfile ? (
|
||||||
justifyContent={'space-between'}
|
<Stack gap={'1rem'} direction={'row'} alignItems={'center'} flex={1}>
|
||||||
alignItems={'center'}
|
<Avatar src={userAvatar} alt={userName} />
|
||||||
width={'100%'}
|
<Typography fontWeight={600}>{userName}</Typography>
|
||||||
>
|
</Stack>
|
||||||
{showProfile ? (
|
) : (
|
||||||
<Stack
|
<StyledAppName>
|
||||||
gap={'1rem'}
|
<AppLogo />
|
||||||
direction={'row'}
|
<span>Nsec.app</span>
|
||||||
alignItems={'center'}
|
</StyledAppName>
|
||||||
flex={1}
|
)}
|
||||||
>
|
|
||||||
<Avatar src={userAvatar} alt={userName} />
|
|
||||||
<Typography fontWeight={600}>{userName}</Typography>
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<StyledAppName>
|
|
||||||
<AppLogo />
|
|
||||||
<span>Nsec.app</span>
|
|
||||||
</StyledAppName>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</StyledAppBar>
|
</StyledAppBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,30 @@
|
|||||||
import { DbKey } from '@/modules/db'
|
import { DbKey } from '@/modules/db'
|
||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
import {
|
import { Avatar, ListItemIcon, MenuItem, Stack, Typography } from '@mui/material'
|
||||||
Avatar,
|
|
||||||
ListItemIcon,
|
|
||||||
MenuItem,
|
|
||||||
Stack,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material'
|
|
||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
type ListProfilesProps = {
|
type ListProfilesProps = {
|
||||||
keys: DbKey[]
|
keys: DbKey[]
|
||||||
onClickItem: (key: DbKey) => void
|
onClickItem: (key: DbKey) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListProfiles: FC<ListProfilesProps> = ({
|
export const ListProfiles: FC<ListProfilesProps> = ({ keys = [], onClickItem }) => {
|
||||||
keys = [],
|
return (
|
||||||
onClickItem,
|
<Stack maxHeight={'10rem'} overflow={'auto'}>
|
||||||
}) => {
|
{keys.map((key) => {
|
||||||
return (
|
const userName = key?.profile?.info?.name || getShortenNpub(key.npub)
|
||||||
<Stack maxHeight={'10rem'} overflow={'auto'}>
|
const userAvatar = key?.profile?.info?.picture || ''
|
||||||
{keys.map((key) => {
|
return (
|
||||||
const userName =
|
<MenuItem sx={{ gap: '0.5rem' }} onClick={() => onClickItem(key)} key={key.npub}>
|
||||||
key?.profile?.info?.name || getShortenNpub(key.npub)
|
<ListItemIcon>
|
||||||
const userAvatar = key?.profile?.info?.picture || ''
|
<Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }} />
|
||||||
return (
|
</ListItemIcon>
|
||||||
<MenuItem
|
<Typography variant="body2" noWrap>
|
||||||
sx={{ gap: '0.5rem' }}
|
{userName}
|
||||||
onClick={() => onClickItem(key)}
|
</Typography>
|
||||||
key={key.npub}
|
</MenuItem>
|
||||||
>
|
)
|
||||||
<ListItemIcon>
|
})}
|
||||||
<Avatar
|
</Stack>
|
||||||
src={userAvatar}
|
)
|
||||||
alt={userName}
|
|
||||||
sx={{ width: 36, height: 36 }}
|
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
|
||||||
<Typography variant='body2' noWrap>
|
|
||||||
{userName}
|
|
||||||
</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,61 +14,46 @@ import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
|
|||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
|
|
||||||
export const Menu = () => {
|
export const Menu = () => {
|
||||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
|
||||||
const isDarkMode = themeMode === 'dark'
|
const isDarkMode = themeMode === 'dark'
|
||||||
const isNoKeys = !keys || keys.length === 0
|
const isNoKeys = !keys || keys.length === 0
|
||||||
|
|
||||||
const {
|
const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
|
||||||
anchorEl,
|
|
||||||
handleClose,
|
|
||||||
handleOpen: handleOpenMenu,
|
|
||||||
open,
|
|
||||||
} = useOpenMenu()
|
|
||||||
|
|
||||||
const handleChangeMode = () => {
|
const handleChangeMode = () => {
|
||||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||||
}
|
}
|
||||||
const handleNavigateToAuth = () => {
|
const handleNavigateToAuth = () => {
|
||||||
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeIcon = isDarkMode ? (
|
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
|
||||||
<DarkModeIcon htmlColor='#fff' />
|
|
||||||
) : (
|
|
||||||
<LightModeIcon htmlColor='#feb94a' />
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuButton onClick={handleOpenMenu}>
|
<MenuButton onClick={handleOpenMenu}>
|
||||||
<MenuRoundedIcon color='inherit' />
|
<MenuRoundedIcon color="inherit" />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MuiMenu
|
<MuiMenu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 1302,
|
zIndex: 1302,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Icon={
|
Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
|
||||||
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
|
onClick={handleNavigateToAuth}
|
||||||
}
|
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||||
onClick={handleNavigateToAuth}
|
/>
|
||||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
|
||||||
/>
|
</MuiMenu>
|
||||||
<MenuItem
|
</>
|
||||||
Icon={themeIcon}
|
)
|
||||||
onClick={handleChangeMode}
|
|
||||||
title='Change theme'
|
|
||||||
/>
|
|
||||||
</MuiMenu>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import React, { FC, ReactNode } from 'react'
|
import React, { FC, ReactNode } from 'react'
|
||||||
import { StyledMenuItem } from './styled'
|
import { StyledMenuItem } from './styled'
|
||||||
import {
|
import { ListItemIcon, MenuItemProps as MuiMenuItemProps, Typography } from '@mui/material'
|
||||||
ListItemIcon,
|
|
||||||
MenuItemProps as MuiMenuItemProps,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
title: string
|
title: string
|
||||||
Icon: ReactNode
|
Icon: ReactNode
|
||||||
} & MuiMenuItemProps
|
} & MuiMenuItemProps
|
||||||
|
|
||||||
export const MenuItem: FC<MenuItemProps> = ({ onClick, Icon, title }) => {
|
export const MenuItem: FC<MenuItemProps> = ({ onClick, Icon, title }) => {
|
||||||
return (
|
return (
|
||||||
<StyledMenuItem onClick={onClick}>
|
<StyledMenuItem onClick={onClick}>
|
||||||
<ListItemIcon>{Icon}</ListItemIcon>
|
<ListItemIcon>{Icon}</ListItemIcon>
|
||||||
<Typography fontWeight={500} variant='body2' noWrap>
|
<Typography fontWeight={500} variant="body2" noWrap>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</StyledMenuItem>
|
</StyledMenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,86 +18,61 @@ import { ListProfiles } from './ListProfiles'
|
|||||||
import { DbKey } from '@/modules/db'
|
import { DbKey } from '@/modules/db'
|
||||||
|
|
||||||
export const ProfileMenu = () => {
|
export const ProfileMenu = () => {
|
||||||
const {
|
const { anchorEl, handleOpen: handleOpenMenu, open, handleClose } = useOpenMenu()
|
||||||
anchorEl,
|
const { handleOpen } = useModalSearchParams()
|
||||||
handleOpen: handleOpenMenu,
|
|
||||||
open,
|
|
||||||
handleClose,
|
|
||||||
} = useOpenMenu()
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
|
||||||
|
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
const isNoKeys = !keys || keys.length === 0
|
const isNoKeys = !keys || keys.length === 0
|
||||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||||
const isDarkMode = themeMode === 'dark'
|
const isDarkMode = themeMode === 'dark'
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleNavigateToAuth = () => {
|
const handleNavigateToAuth = () => {
|
||||||
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNavigateHome = () => {
|
const handleNavigateHome = () => {
|
||||||
navigate('/home')
|
navigate('/home')
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeMode = () => {
|
const handleChangeMode = () => {
|
||||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNavigateToKeyInnerPage = (key: DbKey) => {
|
const handleNavigateToKeyInnerPage = (key: DbKey) => {
|
||||||
navigate('/key/' + key.npub)
|
navigate('/key/' + key.npub)
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeIcon = isDarkMode ? (
|
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
|
||||||
<DarkModeIcon htmlColor='#fff' />
|
|
||||||
) : (
|
|
||||||
<LightModeIcon htmlColor='#feb94a' />
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuButton onClick={handleOpenMenu}>
|
<MenuButton onClick={handleOpenMenu}>
|
||||||
<KeyboardArrowDownRoundedIcon
|
<KeyboardArrowDownRoundedIcon color="inherit" fontSize="large" />
|
||||||
color='inherit'
|
</MenuButton>
|
||||||
fontSize='large'
|
<Menu
|
||||||
/>
|
open={open}
|
||||||
</MenuButton>
|
anchorEl={anchorEl}
|
||||||
<Menu
|
onClose={handleClose}
|
||||||
open={open}
|
sx={{
|
||||||
anchorEl={anchorEl}
|
zIndex: 1302,
|
||||||
onClose={handleClose}
|
}}
|
||||||
sx={{
|
>
|
||||||
zIndex: 1302,
|
<ListProfiles keys={keys} onClickItem={handleNavigateToKeyInnerPage} />
|
||||||
}}
|
<Divider />
|
||||||
>
|
<MenuItem Icon={<HomeRoundedIcon />} onClick={handleNavigateHome} title="Home" />
|
||||||
<ListProfiles
|
<MenuItem
|
||||||
keys={keys}
|
Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
|
||||||
onClickItem={handleNavigateToKeyInnerPage}
|
onClick={handleNavigateToAuth}
|
||||||
/>
|
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||||
<Divider />
|
/>
|
||||||
<MenuItem
|
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
|
||||||
Icon={<HomeRoundedIcon />}
|
</Menu>
|
||||||
onClick={handleNavigateHome}
|
</>
|
||||||
title='Home'
|
)
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
Icon={
|
|
||||||
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
|
|
||||||
}
|
|
||||||
onClick={handleNavigateToAuth}
|
|
||||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
Icon={themeIcon}
|
|
||||||
onClick={handleChangeMode}
|
|
||||||
title='Change theme'
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
import {
|
import { IconButton, IconButtonProps, MenuItem, MenuItemProps, styled } from '@mui/material'
|
||||||
IconButton,
|
|
||||||
IconButtonProps,
|
|
||||||
MenuItem,
|
|
||||||
MenuItemProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export const MenuButton = styled((props: IconButtonProps) => (
|
export const MenuButton = styled((props: IconButtonProps) => <IconButton {...props} />)(({ theme }) => {
|
||||||
<IconButton {...props} />
|
const isDark = theme.palette.mode === 'dark'
|
||||||
))(({ theme }) => {
|
return {
|
||||||
const isDark = theme.palette.mode === 'dark'
|
borderRadius: '1rem',
|
||||||
return {
|
background: isDark ? '#333333A8' : 'transparent',
|
||||||
borderRadius: '1rem',
|
color: isDark ? '#FFFFFFA8' : 'initial',
|
||||||
background: isDark ? '#333333A8' : 'transparent',
|
width: 42,
|
||||||
color: isDark ? '#FFFFFFA8' : 'initial',
|
height: 42,
|
||||||
width: 42,
|
}
|
||||||
height: 42,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledMenuItem = styled((props: MenuItemProps) => (
|
export const StyledMenuItem = styled((props: MenuItemProps) => <MenuItem {...props} />)(() => ({
|
||||||
<MenuItem {...props} />
|
padding: '0.5rem 1rem',
|
||||||
))(() => ({
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,30 +2,30 @@ import { AppBar, Typography, TypographyProps, styled } from '@mui/material'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
||||||
return {
|
return {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
zIndex: 1301,
|
zIndex: 1301,
|
||||||
maxWidth: 'inherit',
|
maxWidth: 'inherit',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledAppName = styled((props: TypographyProps) => (
|
export const StyledAppName = styled((props: TypographyProps) => (
|
||||||
<Typography component={Link} to={'/'} flexGrow={1} {...props} />
|
<Typography component={Link} to={'/'} flexGrow={1} {...props} />
|
||||||
))(() => ({
|
))(() => ({
|
||||||
'&:not(:hover)': {
|
'&:not(:hover)': {
|
||||||
textDecoration: 'initial',
|
textDecoration: 'initial',
|
||||||
},
|
},
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '0.75rem',
|
gap: '0.75rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
lineHeight: '22.4px',
|
lineHeight: '22.4px',
|
||||||
marginLeft: '0.5rem',
|
marginLeft: '0.5rem',
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,45 +1,37 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { Header } from './Header/Header'
|
import { Header } from './Header/Header'
|
||||||
import {
|
import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
|
||||||
Container,
|
|
||||||
ContainerProps,
|
|
||||||
Divider,
|
|
||||||
DividerProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export const Layout: FC = () => {
|
export const Layout: FC = () => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer maxWidth='md'>
|
<StyledContainer maxWidth="md">
|
||||||
<Header />
|
<Header />
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
<main>
|
<main>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledContainer = styled((props: ContainerProps) => (
|
const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="sm" {...props} />)({
|
||||||
<Container maxWidth='sm' {...props} />
|
height: '100%',
|
||||||
))({
|
display: 'flex',
|
||||||
height: '100%',
|
flexDirection: 'column',
|
||||||
display: 'flex',
|
paddingBottom: '1rem',
|
||||||
flexDirection: 'column',
|
position: 'relative',
|
||||||
paddingBottom: '1rem',
|
'& > main': {
|
||||||
position: 'relative',
|
flex: 1,
|
||||||
'& > main': {
|
maxHeight: '100%',
|
||||||
flex: 1,
|
paddingTop: 'calc(66px + 1rem)',
|
||||||
maxHeight: '100%',
|
},
|
||||||
paddingTop: 'calc(66px + 1rem)',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
|
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '66px',
|
top: '66px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
left: 0,
|
left: 0,
|
||||||
height: '2px',
|
height: '2px',
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,217 +2,212 @@ import { MetaEvent } from '@/types/meta-event'
|
|||||||
import Dexie from 'dexie'
|
import Dexie from 'dexie'
|
||||||
|
|
||||||
export interface DbKey {
|
export interface DbKey {
|
||||||
npub: string
|
npub: string
|
||||||
nip05?: string
|
nip05?: string
|
||||||
name?: string
|
name?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
relays?: string[]
|
relays?: string[]
|
||||||
enckey: string
|
enckey: string
|
||||||
profile?: MetaEvent | null
|
profile?: MetaEvent | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbApp {
|
export interface DbApp {
|
||||||
appNpub: string
|
appNpub: string
|
||||||
npub: string
|
npub: string
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
url: string
|
url: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbPerm {
|
export interface DbPerm {
|
||||||
id: string
|
id: string
|
||||||
npub: string
|
npub: string
|
||||||
appNpub: string
|
appNpub: string
|
||||||
perm: string
|
perm: string
|
||||||
value: string
|
value: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbPending {
|
export interface DbPending {
|
||||||
id: string
|
id: string
|
||||||
npub: string
|
npub: string
|
||||||
appNpub: string
|
appNpub: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
method: string
|
method: string
|
||||||
params: string
|
params: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbHistory {
|
export interface DbHistory {
|
||||||
id: string
|
id: string
|
||||||
npub: string
|
npub: string
|
||||||
appNpub: string
|
appNpub: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
method: string
|
method: string
|
||||||
params: string
|
params: string
|
||||||
allowed: boolean
|
allowed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbSyncHistory {
|
export interface DbSyncHistory {
|
||||||
npub: string
|
npub: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DbSchema extends Dexie {
|
export interface DbSchema extends Dexie {
|
||||||
keys: Dexie.Table<DbKey, string>
|
keys: Dexie.Table<DbKey, string>
|
||||||
apps: Dexie.Table<DbApp, string>
|
apps: Dexie.Table<DbApp, string>
|
||||||
perms: Dexie.Table<DbPerm, string>
|
perms: Dexie.Table<DbPerm, string>
|
||||||
pending: Dexie.Table<DbPending, string>
|
pending: Dexie.Table<DbPending, string>
|
||||||
history: Dexie.Table<DbHistory, string>
|
history: Dexie.Table<DbHistory, string>
|
||||||
syncHistory: Dexie.Table<DbSyncHistory, string>
|
syncHistory: Dexie.Table<DbSyncHistory, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = new Dexie('noauthdb') as DbSchema
|
export const db = new Dexie('noauthdb') as DbSchema
|
||||||
|
|
||||||
db.version(8).stores({
|
db.version(8).stores({
|
||||||
keys: 'npub',
|
keys: 'npub',
|
||||||
apps: 'appNpub,npub,name,timestamp',
|
apps: 'appNpub,npub,name,timestamp',
|
||||||
perms: 'id,npub,appNpub,perm,value,timestamp',
|
perms: 'id,npub,appNpub,perm,value,timestamp',
|
||||||
pending: 'id,npub,appNpub,timestamp,method',
|
pending: 'id,npub,appNpub,timestamp,method',
|
||||||
history: 'id,npub,appNpub,timestamp,method,allowed',
|
history: 'id,npub,appNpub,timestamp,method,allowed',
|
||||||
requestHistory: 'id',
|
requestHistory: 'id',
|
||||||
syncHistory: 'npub',
|
syncHistory: 'npub',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const dbi = {
|
export const dbi = {
|
||||||
addKey: async (key: DbKey) => {
|
addKey: async (key: DbKey) => {
|
||||||
try {
|
try {
|
||||||
await db.keys.add(key)
|
await db.keys.add(key)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db addKey error: ${error}`)
|
console.log(`db addKey error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
listKeys: async (): Promise<DbKey[]> => {
|
listKeys: async (): Promise<DbKey[]> => {
|
||||||
try {
|
try {
|
||||||
return await db.keys.toArray()
|
return await db.keys.toArray()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db listKeys error: ${error}`)
|
console.log(`db listKeys error: ${error}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getApp: async (appNpub: string) => {
|
getApp: async (appNpub: string) => {
|
||||||
try {
|
try {
|
||||||
return await db.apps.get(appNpub)
|
return await db.apps.get(appNpub)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db getApp error: ${error}`)
|
console.log(`db getApp error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addApp: async (app: DbApp) => {
|
addApp: async (app: DbApp) => {
|
||||||
try {
|
try {
|
||||||
await db.apps.add(app)
|
await db.apps.add(app)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db addApp error: ${error}`)
|
console.log(`db addApp error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
listApps: async (): Promise<DbApp[]> => {
|
listApps: async (): Promise<DbApp[]> => {
|
||||||
try {
|
try {
|
||||||
return await db.apps.toArray()
|
return await db.apps.toArray()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db listApps error: ${error}`)
|
console.log(`db listApps error: ${error}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeApp: async (appNpub: string) => {
|
removeApp: async (appNpub: string) => {
|
||||||
try {
|
try {
|
||||||
return await db.apps.delete(appNpub)
|
return await db.apps.delete(appNpub)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db removeApp error: ${error}`)
|
console.log(`db removeApp error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addPerm: async (perm: DbPerm) => {
|
addPerm: async (perm: DbPerm) => {
|
||||||
try {
|
try {
|
||||||
await db.perms.add(perm)
|
await db.perms.add(perm)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db addPerm error: ${error}`)
|
console.log(`db addPerm error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
listPerms: async (): Promise<DbPerm[]> => {
|
listPerms: async (): Promise<DbPerm[]> => {
|
||||||
try {
|
try {
|
||||||
return await db.perms.toArray()
|
return await db.perms.toArray()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db listPerms error: ${error}`)
|
console.log(`db listPerms error: ${error}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removePerm: async (id: string) => {
|
removePerm: async (id: string) => {
|
||||||
try {
|
try {
|
||||||
return await db.perms.delete(id)
|
return await db.perms.delete(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db removePerm error: ${error}`)
|
console.log(`db removePerm error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeAppPerms: async (appNpub: string) => {
|
removeAppPerms: async (appNpub: string) => {
|
||||||
try {
|
try {
|
||||||
return await db.perms.where({ appNpub }).delete()
|
return await db.perms.where({ appNpub }).delete()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db removeAppPerms error: ${error}`)
|
console.log(`db removeAppPerms error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addPending: async (r: DbPending) => {
|
addPending: async (r: DbPending) => {
|
||||||
try {
|
try {
|
||||||
return db.transaction('rw', db.pending, db.history, async () => {
|
return db.transaction('rw', db.pending, db.history, async () => {
|
||||||
const exists =
|
const exists =
|
||||||
(await db.pending.where('id').equals(r.id).toArray())
|
(await db.pending.where('id').equals(r.id).toArray()).length > 0 ||
|
||||||
.length > 0 ||
|
(await db.history.where('id').equals(r.id).toArray()).length > 0
|
||||||
(await db.history.where('id').equals(r.id).toArray())
|
if (exists) return false
|
||||||
.length > 0
|
|
||||||
if (exists) return false
|
|
||||||
|
|
||||||
await db.pending.add(r)
|
await db.pending.add(r)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db addPending error: ${error}`)
|
console.log(`db addPending error: ${error}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removePending: async (id: string) => {
|
removePending: async (id: string) => {
|
||||||
try {
|
try {
|
||||||
return await db.pending.delete(id)
|
return await db.pending.delete(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db removePending error: ${error}`)
|
console.log(`db removePending error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
listPending: async (): Promise<DbPending[]> => {
|
listPending: async (): Promise<DbPending[]> => {
|
||||||
try {
|
try {
|
||||||
return await db.pending.toArray()
|
return await db.pending.toArray()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`db listPending error: ${error}`)
|
console.log(`db listPending error: ${error}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmPending: async (id: string, allowed: boolean) => {
|
confirmPending: async (id: string, allowed: boolean) => {
|
||||||
try {
|
try {
|
||||||
db.transaction('rw', db.pending, db.history, async () => {
|
db.transaction('rw', db.pending, db.history, async () => {
|
||||||
const r: DbPending | undefined = await db.pending
|
const r: DbPending | undefined = await db.pending.where('id').equals(id).first()
|
||||||
.where('id')
|
if (!r) throw new Error('Pending not found ' + id)
|
||||||
.equals(id)
|
const h: DbHistory = {
|
||||||
.first()
|
...r,
|
||||||
if (!r) throw new Error('Pending not found ' + id)
|
allowed,
|
||||||
const h: DbHistory = {
|
}
|
||||||
...r,
|
await db.pending.delete(id)
|
||||||
allowed,
|
await db.history.add(h)
|
||||||
}
|
})
|
||||||
await db.pending.delete(id)
|
} catch (error) {
|
||||||
await db.history.add(h)
|
console.log(`db addPending error: ${error}`)
|
||||||
})
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.log(`db addPending error: ${error}`)
|
addConfirmed: async (r: DbHistory) => {
|
||||||
}
|
try {
|
||||||
},
|
await db.history.add(r)
|
||||||
addConfirmed: async (r: DbHistory) => {
|
} catch (error) {
|
||||||
try {
|
console.log(`db addConfirmed error: ${error}`)
|
||||||
await db.history.add(r)
|
return false
|
||||||
} catch (error) {
|
}
|
||||||
console.log(`db addConfirmed error: ${error}`)
|
},
|
||||||
return false
|
addSynced: async (npub: string) => {
|
||||||
}
|
try {
|
||||||
},
|
await db.syncHistory.add({ npub })
|
||||||
addSynced: async (npub: string) => {
|
} catch (error) {
|
||||||
try {
|
console.log(`db addSynced error: ${error}`)
|
||||||
await db.syncHistory.add({ npub })
|
return false
|
||||||
} catch (error) {
|
}
|
||||||
console.log(`db addSynced error: ${error}`)
|
},
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,123 +4,92 @@ ende stands for encryption decryption
|
|||||||
import { secp256k1 as secp } from '@noble/curves/secp256k1'
|
import { secp256k1 as secp } from '@noble/curves/secp256k1'
|
||||||
//import * as secp from "./vendor/secp256k1.js";
|
//import * as secp from "./vendor/secp256k1.js";
|
||||||
|
|
||||||
export async function encrypt(
|
export async function encrypt(publicKey: string, message: string, privateKey: string): Promise<string> {
|
||||||
publicKey: string,
|
const key = secp.getSharedSecret(privateKey, '02' + publicKey)
|
||||||
message: string,
|
const normalizedKey = getNormalizedX(key)
|
||||||
privateKey: string,
|
const encoder = new TextEncoder()
|
||||||
): Promise<string> {
|
const iv = Uint8Array.from(randomBytes(16))
|
||||||
const key = secp.getSharedSecret(privateKey, "02" + publicKey);
|
const plaintext = encoder.encode(message)
|
||||||
const normalizedKey = getNormalizedX(key);
|
const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
||||||
const encoder = new TextEncoder();
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext)
|
||||||
const iv = Uint8Array.from(randomBytes(16));
|
|
||||||
const plaintext = encoder.encode(message);
|
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
normalizedKey,
|
|
||||||
{ name: "AES-CBC" },
|
|
||||||
false,
|
|
||||||
["encrypt"],
|
|
||||||
);
|
|
||||||
const ciphertext = await crypto.subtle.encrypt(
|
|
||||||
{ name: "AES-CBC", iv },
|
|
||||||
cryptoKey,
|
|
||||||
plaintext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ctb64 = toBase64(new Uint8Array(ciphertext));
|
const ctb64 = toBase64(new Uint8Array(ciphertext))
|
||||||
const ivb64 = toBase64(new Uint8Array(iv.buffer));
|
const ivb64 = toBase64(new Uint8Array(iv.buffer))
|
||||||
return `${ctb64}?iv=${ivb64}`;
|
return `${ctb64}?iv=${ivb64}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decrypt(
|
export async function decrypt(privateKey: string, publicKey: string, data: string): Promise<string | Error> {
|
||||||
privateKey: string,
|
const key = secp.getSharedSecret(privateKey, '02' + publicKey) // this line is very slow
|
||||||
publicKey: string,
|
return decrypt_with_shared_secret(data, key)
|
||||||
data: string,
|
|
||||||
): Promise<string | Error> {
|
|
||||||
const key = secp.getSharedSecret(privateKey, "02" + publicKey); // this line is very slow
|
|
||||||
return decrypt_with_shared_secret(data, key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decrypt_with_shared_secret(
|
export async function decrypt_with_shared_secret(data: string, sharedSecret: Uint8Array): Promise<string | Error> {
|
||||||
data: string,
|
const [ctb64, ivb64] = data.split('?iv=')
|
||||||
sharedSecret: Uint8Array,
|
const normalizedKey = getNormalizedX(sharedSecret)
|
||||||
): Promise<string | Error> {
|
|
||||||
const [ctb64, ivb64] = data.split("?iv=");
|
|
||||||
const normalizedKey = getNormalizedX(sharedSecret);
|
|
||||||
|
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['decrypt'])
|
||||||
"raw",
|
let ciphertext: BufferSource
|
||||||
normalizedKey,
|
let iv: BufferSource
|
||||||
{ name: "AES-CBC" },
|
try {
|
||||||
false,
|
ciphertext = decodeBase64(ctb64)
|
||||||
["decrypt"],
|
iv = decodeBase64(ivb64)
|
||||||
);
|
} catch (e) {
|
||||||
let ciphertext: BufferSource;
|
return new Error(`failed to decode, ${e}`)
|
||||||
let iv: BufferSource;
|
}
|
||||||
try {
|
|
||||||
ciphertext = decodeBase64(ctb64);
|
|
||||||
iv = decodeBase64(ivb64);
|
|
||||||
} catch (e) {
|
|
||||||
return new Error(`failed to decode, ${e}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const plaintext = await crypto.subtle.decrypt(
|
const plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
|
||||||
{ name: "AES-CBC", iv },
|
const text = utf8Decode(plaintext)
|
||||||
cryptoKey,
|
return text
|
||||||
ciphertext,
|
} catch (e) {
|
||||||
);
|
return new Error(`failed to decrypt, ${e}`)
|
||||||
const text = utf8Decode(plaintext);
|
}
|
||||||
return text;
|
|
||||||
} catch (e) {
|
|
||||||
return new Error(`failed to decrypt, ${e}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function utf8Encode(str: string) {
|
export function utf8Encode(str: string) {
|
||||||
let encoder = new TextEncoder();
|
let encoder = new TextEncoder()
|
||||||
return encoder.encode(str);
|
return encoder.encode(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function utf8Decode(bin: Uint8Array | ArrayBuffer): string {
|
export function utf8Decode(bin: Uint8Array | ArrayBuffer): string {
|
||||||
let decoder = new TextDecoder();
|
let decoder = new TextDecoder()
|
||||||
return decoder.decode(bin);
|
return decoder.decode(bin)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBase64(uInt8Array: Uint8Array) {
|
function toBase64(uInt8Array: Uint8Array) {
|
||||||
let strChunks = new Array(uInt8Array.length);
|
let strChunks = new Array(uInt8Array.length)
|
||||||
let i = 0;
|
let i = 0
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for (let byte of uInt8Array) {
|
for (let byte of uInt8Array) {
|
||||||
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
|
strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
|
||||||
i++;
|
i++
|
||||||
}
|
}
|
||||||
return btoa(strChunks.join(""));
|
return btoa(strChunks.join(''))
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBase64(base64String: string) {
|
function decodeBase64(base64String: string) {
|
||||||
const binaryString = atob(base64String);
|
const binaryString = atob(base64String)
|
||||||
const length = binaryString.length;
|
const length = binaryString.length
|
||||||
const bytes = new Uint8Array(length);
|
const bytes = new Uint8Array(length)
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
bytes[i] = binaryString.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNormalizedX(key: Uint8Array): Uint8Array {
|
function getNormalizedX(key: Uint8Array): Uint8Array {
|
||||||
return key.slice(1, 33);
|
return key.slice(1, 33)
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomBytes(bytesLength: number = 32) {
|
function randomBytes(bytesLength: number = 32) {
|
||||||
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
return crypto.getRandomValues(new Uint8Array(bytesLength))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function utf16Encode(str: string): number[] {
|
export function utf16Encode(str: string): number[] {
|
||||||
let array = new Array(str.length);
|
let array = new Array(str.length)
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
array[i] = str.charCodeAt(i);
|
array[i] = str.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return array;
|
return array
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import crypto, { pbkdf2 } from 'crypto';
|
import crypto, { pbkdf2 } from 'crypto'
|
||||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
// encrypted keys have a prefix and version
|
// encrypted keys have a prefix and version
|
||||||
// so that we'd be able to switch to a better
|
// so that we'd be able to switch to a better
|
||||||
@@ -17,14 +17,14 @@ const ITERATIONS_PWH = 100000
|
|||||||
const HASH_SIZE = 32
|
const HASH_SIZE = 32
|
||||||
const HASH_ALGO = 'sha256'
|
const HASH_ALGO = 'sha256'
|
||||||
// encryption
|
// encryption
|
||||||
const ALGO = 'aes-256-cbc';
|
const ALGO = 'aes-256-cbc'
|
||||||
const IV_SIZE = 16
|
const IV_SIZE = 16
|
||||||
|
|
||||||
// valid passwords are a limited ASCII only, see notes below
|
// valid passwords are a limited ASCII only, see notes below
|
||||||
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/
|
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/
|
||||||
|
|
||||||
const ALGO_LOCAL = 'AES-CBC';
|
const ALGO_LOCAL = 'AES-CBC'
|
||||||
const KEY_SIZE_LOCAL = 256;
|
const KEY_SIZE_LOCAL = 256
|
||||||
|
|
||||||
export class Keys {
|
export class Keys {
|
||||||
subtle: any
|
subtle: any
|
||||||
@@ -37,9 +37,7 @@ export class Keys {
|
|||||||
return ASCII_REGEX.test(passphrase)
|
return ASCII_REGEX.test(passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generatePassKey(pubkey: string, passphrase: string)
|
public async generatePassKey(pubkey: string, passphrase: string): Promise<{ passkey: Buffer; pwh: string }> {
|
||||||
: Promise<{ passkey: Buffer, pwh: string }> {
|
|
||||||
|
|
||||||
const salt = Buffer.from(pubkey, 'hex')
|
const salt = Buffer.from(pubkey, 'hex')
|
||||||
|
|
||||||
// https://nodejs.org/api/crypto.html#using-strings-as-inputs-to-cryptographic-apis
|
// https://nodejs.org/api/crypto.html#using-strings-as-inputs-to-cryptographic-apis
|
||||||
@@ -47,7 +45,7 @@ export class Keys {
|
|||||||
// We could use string.normalize() to make sure all JS implementations
|
// We could use string.normalize() to make sure all JS implementations
|
||||||
// are compatible, but since we're looking to make this thing a standard
|
// are compatible, but since we're looking to make this thing a standard
|
||||||
// then the simplest way is to exclude unicode and only work with ASCII
|
// then the simplest way is to exclude unicode and only work with ASCII
|
||||||
if (!this.isValidPassphase(passphrase)) throw new Error("Password must be 4+ ASCII chars")
|
if (!this.isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
|
||||||
|
|
||||||
return new Promise((ok, fail) => {
|
return new Promise((ok, fail) => {
|
||||||
// NOTE: we should use Argon2 or scrypt later, for now
|
// NOTE: we should use Argon2 or scrypt later, for now
|
||||||
@@ -57,7 +55,11 @@ export class Keys {
|
|||||||
else {
|
else {
|
||||||
pbkdf2(key, passphrase, ITERATIONS_PWH, HASH_SIZE, HASH_ALGO, (err, hash) => {
|
pbkdf2(key, passphrase, ITERATIONS_PWH, HASH_SIZE, HASH_ALGO, (err, hash) => {
|
||||||
if (err) fail(err)
|
if (err) fail(err)
|
||||||
else ok({ passkey: key, pwh: hash.toString('hex') })
|
else
|
||||||
|
ok({
|
||||||
|
passkey: key,
|
||||||
|
pwh: hash.toString('hex'),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -65,8 +67,8 @@ export class Keys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isSafari() {
|
private isSafari() {
|
||||||
const chrome = navigator.userAgent.indexOf("Chrome") > -1;
|
const chrome = navigator.userAgent.indexOf('Chrome') > -1
|
||||||
const safari = navigator.userAgent.indexOf("Safari") > -1;
|
const safari = navigator.userAgent.indexOf('Safari') > -1
|
||||||
return safari && !chrome
|
return safari && !chrome
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +83,8 @@ export class Keys {
|
|||||||
{ name: ALGO_LOCAL, length: KEY_SIZE_LOCAL },
|
{ name: ALGO_LOCAL, length: KEY_SIZE_LOCAL },
|
||||||
// NOTE: important to make sure it's not visible in
|
// NOTE: important to make sure it's not visible in
|
||||||
// dev console in IndexedDB
|
// dev console in IndexedDB
|
||||||
/*extractable*/false,
|
/*extractable*/ false,
|
||||||
["encrypt", "decrypt"]
|
['encrypt', 'decrypt']
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,25 +96,30 @@ export class Keys {
|
|||||||
return `${PREFIX_LOCAL}:${VERSION_LOCAL}:${iv.toString('hex')}:${Buffer.from(encrypted).toString('hex')}}`
|
return `${PREFIX_LOCAL}:${VERSION_LOCAL}:${iv.toString('hex')}:${Buffer.from(encrypted).toString('hex')}}`
|
||||||
}
|
}
|
||||||
|
|
||||||
public async decryptKeyLocal({ enckey, localKey }: { enckey: string, localKey: CryptoKey | {} }): Promise<string> {
|
public async decryptKeyLocal({ enckey, localKey }: { enckey: string; localKey: CryptoKey | {} }): Promise<string> {
|
||||||
if (this.isSafari()) return enckey
|
if (this.isSafari()) return enckey
|
||||||
const parts = enckey.split(':')
|
const parts = enckey.split(':')
|
||||||
if (parts.length !== 4) throw new Error("Bad encrypted key")
|
if (parts.length !== 4) throw new Error('Bad encrypted key')
|
||||||
if (parts[0] !== PREFIX_LOCAL) throw new Error("Bad encrypted key prefix")
|
if (parts[0] !== PREFIX_LOCAL) throw new Error('Bad encrypted key prefix')
|
||||||
if (parts[1] !== VERSION_LOCAL) throw new Error("Bad encrypted key version")
|
if (parts[1] !== VERSION_LOCAL) throw new Error('Bad encrypted key version')
|
||||||
if (parts[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv")
|
if (parts[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
|
||||||
if (parts[3].length < 30) throw new Error("Bad encrypted key data")
|
if (parts[3].length < 30) throw new Error('Bad encrypted key data')
|
||||||
const iv = Buffer.from(parts[2], 'hex');
|
const iv = Buffer.from(parts[2], 'hex')
|
||||||
const data = Buffer.from(parts[3], 'hex');
|
const data = Buffer.from(parts[3], 'hex')
|
||||||
const decrypted = await this.subtle.decrypt({ name: ALGO_LOCAL, iv }, localKey, data)
|
const decrypted = await this.subtle.decrypt({ name: ALGO_LOCAL, iv }, localKey, data)
|
||||||
const { type, data: value } = nip19.decode(Buffer.from(decrypted).toString())
|
const { type, data: value } = nip19.decode(Buffer.from(decrypted).toString())
|
||||||
if (type !== "nsec") throw new Error("Bad encrypted key payload type")
|
if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
|
||||||
if ((value as string).length !== 64) throw new Error("Bad encrypted key payload length")
|
if ((value as string).length !== 64) throw new Error('Bad encrypted key payload length')
|
||||||
return (value as string)
|
return value as string
|
||||||
}
|
}
|
||||||
|
|
||||||
public async encryptKeyPass({ key, passphrase }: { key: string, passphrase: string })
|
public async encryptKeyPass({
|
||||||
: Promise<{ enckey: string, pwh: string }> {
|
key,
|
||||||
|
passphrase,
|
||||||
|
}: {
|
||||||
|
key: string
|
||||||
|
passphrase: string
|
||||||
|
}): Promise<{ enckey: string; pwh: string }> {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
const nsec = nip19.nsecEncode(key)
|
const nsec = nip19.nsecEncode(key)
|
||||||
const pubkey = getPublicKey(key)
|
const pubkey = getPublicKey(key)
|
||||||
@@ -120,21 +127,29 @@ export class Keys {
|
|||||||
const iv = crypto.randomBytes(IV_SIZE)
|
const iv = crypto.randomBytes(IV_SIZE)
|
||||||
const cipher = crypto.createCipheriv(ALGO, passkey, iv)
|
const cipher = crypto.createCipheriv(ALGO, passkey, iv)
|
||||||
const encrypted = Buffer.concat([cipher.update(nsec), cipher.final()])
|
const encrypted = Buffer.concat([cipher.update(nsec), cipher.final()])
|
||||||
console.log("encrypted key in ", Date.now() - start)
|
console.log('encrypted key in ', Date.now() - start)
|
||||||
return {
|
return {
|
||||||
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`,
|
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`,
|
||||||
pwh
|
pwh,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async decryptKeyPass({ pubkey, enckey, passphrase }: { pubkey: string, enckey: string, passphrase: string }): Promise<string> {
|
public async decryptKeyPass({
|
||||||
|
pubkey,
|
||||||
|
enckey,
|
||||||
|
passphrase,
|
||||||
|
}: {
|
||||||
|
pubkey: string
|
||||||
|
enckey: string
|
||||||
|
passphrase: string
|
||||||
|
}): Promise<string> {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
const parts = enckey.split(':')
|
const parts = enckey.split(':')
|
||||||
if (parts.length !== 4) throw new Error("Bad encrypted key")
|
if (parts.length !== 4) throw new Error('Bad encrypted key')
|
||||||
if (parts[0] !== PREFIX) throw new Error("Bad encrypted key prefix")
|
if (parts[0] !== PREFIX) throw new Error('Bad encrypted key prefix')
|
||||||
if (parts[1] !== VERSION) throw new Error("Bad encrypted key version")
|
if (parts[1] !== VERSION) throw new Error('Bad encrypted key version')
|
||||||
if (parts[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv")
|
if (parts[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
|
||||||
if (parts[3].length < 30) throw new Error("Bad encrypted key data")
|
if (parts[3].length < 30) throw new Error('Bad encrypted key data')
|
||||||
const { passkey } = await this.generatePassKey(pubkey, passphrase)
|
const { passkey } = await this.generatePassKey(pubkey, passphrase)
|
||||||
const iv = Buffer.from(parts[2], 'hex')
|
const iv = Buffer.from(parts[2], 'hex')
|
||||||
const data = Buffer.from(parts[3], 'hex')
|
const data = Buffer.from(parts[3], 'hex')
|
||||||
@@ -142,9 +157,9 @@ export class Keys {
|
|||||||
const decrypted = Buffer.concat([decipher.update(data), decipher.final()])
|
const decrypted = Buffer.concat([decipher.update(data), decipher.final()])
|
||||||
const nsec = decrypted.toString()
|
const nsec = decrypted.toString()
|
||||||
const { type, data: value } = nip19.decode(nsec)
|
const { type, data: value } = nip19.decode(nsec)
|
||||||
if (type !== "nsec") throw new Error("Bad encrypted key payload type")
|
if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
|
||||||
if (value.length !== 64) throw new Error("Bad encrypted key payload length")
|
if (value.length !== 64) throw new Error('Bad encrypted key payload length')
|
||||||
console.log("decrypted key in ", Date.now() - start)
|
console.log('decrypted key in ', Date.now() - start)
|
||||||
return nsec;
|
return nsec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ export const utf8Decoder = new TextDecoder('utf-8')
|
|||||||
export const utf8Encoder = new TextEncoder()
|
export const utf8Encoder = new TextEncoder()
|
||||||
|
|
||||||
function toBase64(uInt8Array: Uint8Array) {
|
function toBase64(uInt8Array: Uint8Array) {
|
||||||
let strChunks = new Array(uInt8Array.length);
|
let strChunks = new Array(uInt8Array.length)
|
||||||
let i = 0;
|
let i = 0
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for (let byte of uInt8Array) {
|
for (let byte of uInt8Array) {
|
||||||
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
|
strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
|
||||||
i++;
|
i++
|
||||||
}
|
}
|
||||||
return btoa(strChunks.join(""));
|
return btoa(strChunks.join(''))
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromBase64(base64String: string) {
|
function fromBase64(base64String: string) {
|
||||||
const binaryString = atob(base64String);
|
const binaryString = atob(base64String)
|
||||||
const length = binaryString.length;
|
const length = binaryString.length
|
||||||
const bytes = new Uint8Array(length);
|
const bytes = new Uint8Array(length)
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
bytes[i] = binaryString.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNormalizedX(key: Uint8Array): Uint8Array {
|
function getNormalizedX(key: Uint8Array): Uint8Array {
|
||||||
@@ -65,7 +65,7 @@ export class Nip04 {
|
|||||||
// let ctb64 = toBase64(new Uint8Array(ciphertext))
|
// let ctb64 = toBase64(new Uint8Array(ciphertext))
|
||||||
// let ivb64 = toBase64(new Uint8Array(iv.buffer))
|
// let ivb64 = toBase64(new Uint8Array(iv.buffer))
|
||||||
|
|
||||||
console.log("nip04_encrypt", text, "t1", t2 - t1, "t2", t3 - t2, "t3", Date.now() - t3)
|
console.log('nip04_encrypt', text, 't1', t2 - t1, 't2', t3 - t2, 't3', Date.now() - t3)
|
||||||
|
|
||||||
return `${ctb64}?iv=${ivb64}`
|
return `${ctb64}?iv=${ivb64}`
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,4 @@ export class Nip04 {
|
|||||||
let text = utf8Decoder.decode(plaintext)
|
let text = utf8Decoder.decode(plaintext)
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,91 +5,79 @@ import NDK, { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk'
|
|||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
export const ndk = new NDK({
|
export const ndk = new NDK({
|
||||||
explicitRelayUrls: [
|
explicitRelayUrls: ['wss://relay.nostr.band/all', 'wss://relay.nostr.band', 'wss://relay.damus.io', 'wss://nos.lol'],
|
||||||
'wss://relay.nostr.band/all',
|
|
||||||
'wss://relay.nostr.band',
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export function nostrEvent(e: Required<NDKEvent>) {
|
export function nostrEvent(e: Required<NDKEvent>) {
|
||||||
return {
|
return {
|
||||||
id: e.id,
|
id: e.id,
|
||||||
created_at: e.created_at,
|
created_at: e.created_at,
|
||||||
pubkey: e.pubkey,
|
pubkey: e.pubkey,
|
||||||
kind: e.kind,
|
kind: e.kind,
|
||||||
tags: e.tags,
|
tags: e.tags,
|
||||||
content: e.content,
|
content: e.content,
|
||||||
sig: e.sig,
|
sig: e.sig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function rawEvent(e: Required<NDKEvent>): AugmentedEvent {
|
function rawEvent(e: Required<NDKEvent>): AugmentedEvent {
|
||||||
return {
|
return {
|
||||||
...nostrEvent(e),
|
...nostrEvent(e),
|
||||||
identifier: getTagValue(e as NDKEvent, 'd'),
|
identifier: getTagValue(e as NDKEvent, 'd'),
|
||||||
order: e.created_at as number,
|
order: e.created_at as number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseContentJson(c: string): object {
|
function parseContentJson(c: string): object {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(c)
|
return JSON.parse(c)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Bad json: ', c, e)
|
console.log('Bad json: ', c, e)
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTags(
|
export function getTags(e: AugmentedEvent | NDKEvent | MetaEvent, name: string): string[][] {
|
||||||
e: AugmentedEvent | NDKEvent | MetaEvent,
|
return e.tags.filter((t: string[]) => t.length > 0 && t[0] === name)
|
||||||
name: string,
|
|
||||||
): string[][] {
|
|
||||||
return e.tags.filter((t: string[]) => t.length > 0 && t[0] === name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTag(
|
export function getTag(e: AugmentedEvent | NDKEvent | MetaEvent, name: string): string[] | null {
|
||||||
e: AugmentedEvent | NDKEvent | MetaEvent,
|
const tags = getTags(e, name)
|
||||||
name: string,
|
if (tags.length === 0) return null
|
||||||
): string[] | null {
|
return tags[0]
|
||||||
const tags = getTags(e, name)
|
|
||||||
if (tags.length === 0) return null
|
|
||||||
return tags[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTagValue(
|
export function getTagValue(
|
||||||
e: AugmentedEvent | NDKEvent | MetaEvent,
|
e: AugmentedEvent | NDKEvent | MetaEvent,
|
||||||
name: string,
|
name: string,
|
||||||
index: number = 0,
|
index: number = 0,
|
||||||
def: string = '',
|
def: string = ''
|
||||||
): string {
|
): string {
|
||||||
const tag = getTag(e, name)
|
const tag = getTag(e, name)
|
||||||
if (tag === null || !tag.length || (index && index >= tag.length))
|
if (tag === null || !tag.length || (index && index >= tag.length)) return def
|
||||||
return def
|
return tag[1 + index]
|
||||||
return tag[1 + index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseProfileJson(e: NostrEvent): Meta {
|
export function parseProfileJson(e: NostrEvent): Meta {
|
||||||
// all meta fields are optional so 'as' works fine
|
// all meta fields are optional so 'as' works fine
|
||||||
const profile = createMeta(parseContentJson(e.content))
|
const profile = createMeta(parseContentJson(e.content))
|
||||||
profile.pubkey = e.pubkey
|
profile.pubkey = e.pubkey
|
||||||
profile.npub = nip19.npubEncode(e.pubkey)
|
profile.npub = nip19.npubEncode(e.pubkey)
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchProfile(npub: string): Promise<MetaEvent | null> {
|
export async function fetchProfile(npub: string): Promise<MetaEvent | null> {
|
||||||
const npubToken = npub.includes('#') ? npub.split('#')[0] : npub
|
const npubToken = npub.includes('#') ? npub.split('#')[0] : npub
|
||||||
const { type, data: pubkey } = nip19.decode(npubToken)
|
const { type, data: pubkey } = nip19.decode(npubToken)
|
||||||
if (type !== 'npub') return null
|
if (type !== 'npub') return null
|
||||||
|
|
||||||
const event = await ndk.fetchEvent({ kinds: [0], authors: [pubkey] })
|
const event = await ndk.fetchEvent({ kinds: [0], authors: [pubkey] })
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
const augmentedEvent = rawEvent(event as Required<NDKEvent>)
|
const augmentedEvent = rawEvent(event as Required<NDKEvent>)
|
||||||
const m = createMetaEvent(augmentedEvent)
|
const m = createMetaEvent(augmentedEvent)
|
||||||
m.info = parseProfileJson(m)
|
m.info = parseProfileJson(m)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
// based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts
|
// based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts
|
||||||
|
|
||||||
import { sha256 } from "@noble/hashes/sha256";
|
import { sha256 } from '@noble/hashes/sha256'
|
||||||
import { bytesToHex } from "@noble/hashes/utils";
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
|
||||||
export interface NostrPowEvent {
|
export interface NostrPowEvent {
|
||||||
id?: string;
|
id?: string
|
||||||
pubkey: string;
|
pubkey: string
|
||||||
created_at: number;
|
created_at: number
|
||||||
kind?: number;
|
kind?: number
|
||||||
tags: Array<Array<string>>;
|
tags: Array<Array<string>>
|
||||||
content: string;
|
content: string
|
||||||
sig?: string;
|
sig?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function minePow(e: NostrPowEvent, target: number) {
|
export function minePow(e: NostrPowEvent, target: number) {
|
||||||
let ctr = 0;
|
let ctr = 0
|
||||||
|
|
||||||
let nonceTagIdx = e.tags.findIndex(a => a[0] === "nonce");
|
let nonceTagIdx = e.tags.findIndex((a) => a[0] === 'nonce')
|
||||||
if (nonceTagIdx === -1) {
|
if (nonceTagIdx === -1) {
|
||||||
nonceTagIdx = e.tags.length;
|
nonceTagIdx = e.tags.length
|
||||||
e.tags.push(["nonce", ctr.toString(), target.toString()]);
|
e.tags.push(['nonce', ctr.toString(), target.toString()])
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
e.tags[nonceTagIdx][1] = (++ctr).toString();
|
e.tags[nonceTagIdx][1] = (++ctr).toString()
|
||||||
e.id = createId(e);
|
e.id = createId(e)
|
||||||
} while (countLeadingZeros(e.id) < target);
|
} while (countLeadingZeros(e.id) < target)
|
||||||
|
|
||||||
return e;
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
function createId(e: NostrPowEvent) {
|
function createId(e: NostrPowEvent) {
|
||||||
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content];
|
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content]
|
||||||
return bytesToHex(sha256(JSON.stringify(payload)));
|
return bytesToHex(sha256(JSON.stringify(payload)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function countLeadingZeros(hex: string) {
|
export function countLeadingZeros(hex: string) {
|
||||||
let count = 0;
|
let count = 0
|
||||||
|
|
||||||
for (let i = 0; i < hex.length; i++) {
|
for (let i = 0; i < hex.length; i++) {
|
||||||
const nibble = parseInt(hex[i], 16);
|
const nibble = parseInt(hex[i], 16)
|
||||||
if (nibble === 0) {
|
if (nibble === 0) {
|
||||||
count += 4;
|
count += 4
|
||||||
} else {
|
} else {
|
||||||
count += Math.clz32(nibble) - 28;
|
count += Math.clz32(nibble) - 28
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,67 +8,63 @@ import { Nip04 } from './nip04'
|
|||||||
//import { decrypt, encrypt } from "./ende";
|
//import { decrypt, encrypt } from "./ende";
|
||||||
|
|
||||||
export class PrivateKeySigner implements NDKSigner {
|
export class PrivateKeySigner implements NDKSigner {
|
||||||
private _user: NDKUser | undefined
|
private _user: NDKUser | undefined
|
||||||
privateKey?: string
|
privateKey?: string
|
||||||
private nip04: Nip04
|
private nip04: Nip04
|
||||||
|
|
||||||
public constructor(privateKey?: string) {
|
public constructor(privateKey?: string) {
|
||||||
if (privateKey) {
|
if (privateKey) {
|
||||||
this.privateKey = privateKey
|
this.privateKey = privateKey
|
||||||
this._user = new NDKUser({
|
this._user = new NDKUser({
|
||||||
hexpubkey: getPublicKey(this.privateKey),
|
hexpubkey: getPublicKey(this.privateKey),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.nip04 = new Nip04()
|
this.nip04 = new Nip04()
|
||||||
}
|
}
|
||||||
|
|
||||||
public static generate() {
|
public static generate() {
|
||||||
const privateKey = generatePrivateKey()
|
const privateKey = generatePrivateKey()
|
||||||
return new PrivateKeySigner(privateKey)
|
return new PrivateKeySigner(privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async blockUntilReady(): Promise<NDKUser> {
|
public async blockUntilReady(): Promise<NDKUser> {
|
||||||
if (!this._user) {
|
if (!this._user) {
|
||||||
throw new Error('NDKUser not initialized')
|
throw new Error('NDKUser not initialized')
|
||||||
}
|
}
|
||||||
return this._user
|
return this._user
|
||||||
}
|
}
|
||||||
|
|
||||||
public async user(): Promise<NDKUser> {
|
public async user(): Promise<NDKUser> {
|
||||||
await this.blockUntilReady()
|
await this.blockUntilReady()
|
||||||
return this._user as NDKUser
|
return this._user as NDKUser
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sign(event: NostrEvent): Promise<string> {
|
public async sign(event: NostrEvent): Promise<string> {
|
||||||
if (!this.privateKey) {
|
if (!this.privateKey) {
|
||||||
throw Error('Attempted to sign without a private key')
|
throw Error('Attempted to sign without a private key')
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSignature(event as UnsignedEvent, this.privateKey)
|
return getSignature(event as UnsignedEvent, this.privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async encrypt(recipient: NDKUser, value: string): Promise<string> {
|
public async encrypt(recipient: NDKUser, value: string): Promise<string> {
|
||||||
if (!this.privateKey) {
|
if (!this.privateKey) {
|
||||||
throw Error('Attempted to encrypt without a private key')
|
throw Error('Attempted to encrypt without a private key')
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientHexPubKey = recipient.hexpubkey
|
const recipientHexPubKey = recipient.hexpubkey
|
||||||
return await this.nip04.encrypt(
|
return await this.nip04.encrypt(this.privateKey, recipientHexPubKey, value)
|
||||||
this.privateKey,
|
// return await encrypt(recipientHexPubKey, value, this.privateKey);
|
||||||
recipientHexPubKey,
|
}
|
||||||
value,
|
|
||||||
)
|
|
||||||
// return await encrypt(recipientHexPubKey, value, this.privateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async decrypt(sender: NDKUser, value: string): Promise<string> {
|
public async decrypt(sender: NDKUser, value: string): Promise<string> {
|
||||||
if (!this.privateKey) {
|
if (!this.privateKey) {
|
||||||
throw Error('Attempted to decrypt without a private key')
|
throw Error('Attempted to decrypt without a private key')
|
||||||
}
|
}
|
||||||
|
|
||||||
const senderHexPubKey = sender.hexpubkey
|
const senderHexPubKey = sender.hexpubkey
|
||||||
// console.log("nip04_decrypt", value)
|
// console.log("nip04_decrypt", value)
|
||||||
return await this.nip04.decrypt(this.privateKey, senderHexPubKey, value)
|
return await this.nip04.decrypt(this.privateKey, senderHexPubKey, value)
|
||||||
// return await decrypt(this.privateKey, senderHexPubKey, value) as string;
|
// return await decrypt(this.privateKey, senderHexPubKey, value) as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,74 +7,72 @@ let nextReqId = 1
|
|||||||
let onRender: (() => void) | null = null
|
let onRender: (() => void) | null = null
|
||||||
|
|
||||||
export async function swicRegister() {
|
export async function swicRegister() {
|
||||||
serviceWorkerRegistration.register({
|
serviceWorkerRegistration.register({
|
||||||
onSuccess(registration) {
|
onSuccess(registration) {
|
||||||
console.log('sw registered')
|
console.log('sw registered')
|
||||||
swr = registration
|
swr = registration
|
||||||
},
|
},
|
||||||
onError(e) {
|
onError(e) {
|
||||||
console.log(`error ${e}`)
|
console.log(`error ${e}`)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
navigator.serviceWorker.ready.then((r) => {
|
navigator.serviceWorker.ready.then((r) => {
|
||||||
console.log("sw ready")
|
console.log('sw ready')
|
||||||
swr = r
|
swr = r
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
console.log(
|
console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
|
||||||
`This page is currently controlled by: ${navigator.serviceWorker.controller}`,
|
} else {
|
||||||
);
|
console.log('This page is not currently controlled by a service worker.')
|
||||||
} else {
|
}
|
||||||
console.log("This page is not currently controlled by a service worker.");
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||||
onMessage((event as MessageEvent).data)
|
onMessage((event as MessageEvent).data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessage(data: any) {
|
function onMessage(data: any) {
|
||||||
const { id, result, error } = data
|
const { id, result, error } = data
|
||||||
console.log('SW message', id, result, error)
|
console.log('SW message', id, result, error)
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
if (onRender) onRender()
|
if (onRender) onRender()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = reqs.get(id)
|
const r = reqs.get(id)
|
||||||
if (!r) {
|
if (!r) {
|
||||||
console.log('Unexpected message from service worker', data)
|
console.log('Unexpected message from service worker', data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reqs.delete(id)
|
reqs.delete(id)
|
||||||
if (error) r.rej(error)
|
if (error) r.rej(error)
|
||||||
else r.ok(result)
|
else r.ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function swicCall(method: string, ...args: any[]) {
|
export async function swicCall(method: string, ...args: any[]) {
|
||||||
const id = nextReqId
|
const id = nextReqId
|
||||||
nextReqId++
|
nextReqId++
|
||||||
|
|
||||||
return new Promise((ok, rej) => {
|
return new Promise((ok, rej) => {
|
||||||
if (!swr || !swr.active) {
|
if (!swr || !swr.active) {
|
||||||
rej(new Error('No active service worker'))
|
rej(new Error('No active service worker'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reqs.set(id, { ok, rej })
|
reqs.set(id, { ok, rej })
|
||||||
const msg = {
|
const msg = {
|
||||||
id,
|
id,
|
||||||
method,
|
method,
|
||||||
args: [...args],
|
args: [...args],
|
||||||
}
|
}
|
||||||
console.log('sending to SW', msg)
|
console.log('sending to SW', msg)
|
||||||
swr.active.postMessage(msg)
|
swr.active.postMessage(msg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swicOnRender(cb: () => void) {
|
export function swicOnRender(cb: () => void) {
|
||||||
onRender = cb
|
onRender = cb
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import { darkTheme, lightTheme } from './theme'
|
|||||||
import { useAppSelector } from '../../store/hooks/redux'
|
import { useAppSelector } from '../../store/hooks/redux'
|
||||||
|
|
||||||
const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||||
const isDarkMode = themeMode === 'dark'
|
const isDarkMode = themeMode === 'dark'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeMuiProvider theme={isDarkMode ? darkTheme : lightTheme}>
|
<ThemeMuiProvider theme={isDarkMode ? darkTheme : lightTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{children}
|
{children}
|
||||||
</ThemeMuiProvider>
|
</ThemeMuiProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThemeProvider
|
export default ThemeProvider
|
||||||
|
|||||||
@@ -1,99 +1,99 @@
|
|||||||
import { createTheme, Theme } from '@mui/material'
|
import { createTheme, Theme } from '@mui/material'
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
declare module '@mui/material/styles' {
|
||||||
interface Palette {
|
interface Palette {
|
||||||
textSecondaryDecorate: Palette['primary']
|
textSecondaryDecorate: Palette['primary']
|
||||||
backgroundSecondary: Palette['background']
|
backgroundSecondary: Palette['background']
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaletteOptions {
|
interface PaletteOptions {
|
||||||
textSecondaryDecorate?: Palette['primary']
|
textSecondaryDecorate?: Palette['primary']
|
||||||
backgroundSecondary?: Palette['background']
|
backgroundSecondary?: Palette['background']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const commonTheme: Theme = createTheme({
|
const commonTheme: Theme = createTheme({
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: ['Inter', 'sans-serif'].join(','),
|
fontFamily: ['Inter', 'sans-serif'].join(','),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
textTransform: 'initial',
|
textTransform: 'initial',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const lightTheme: Theme = createTheme({
|
const lightTheme: Theme = createTheme({
|
||||||
...commonTheme,
|
...commonTheme,
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'light',
|
mode: 'light',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#000000',
|
main: '#000000',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#E8E9EB',
|
main: '#E8E9EB',
|
||||||
dark: '#ACACAC',
|
dark: '#ACACAC',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#f44336',
|
main: '#f44336',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
default: '#f7f7f7',
|
default: '#f7f7f7',
|
||||||
paper: '#f7f7f7',
|
paper: '#f7f7f7',
|
||||||
},
|
},
|
||||||
backgroundSecondary: {
|
backgroundSecondary: {
|
||||||
default: '#E8E9EB',
|
default: '#E8E9EB',
|
||||||
paper: '#f7f7f7',
|
paper: '#f7f7f7',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#000000',
|
primary: '#000000',
|
||||||
secondary: '#ffffff',
|
secondary: '#ffffff',
|
||||||
},
|
},
|
||||||
textSecondaryDecorate: {
|
textSecondaryDecorate: {
|
||||||
main: '#6b6b6b',
|
main: '#6b6b6b',
|
||||||
light: '#000',
|
light: '#000',
|
||||||
dark: '#000',
|
dark: '#000',
|
||||||
contrastText: '#000',
|
contrastText: '#000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const darkTheme: Theme = createTheme({
|
const darkTheme: Theme = createTheme({
|
||||||
...commonTheme,
|
...commonTheme,
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#FFFFFF',
|
main: '#FFFFFF',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#222222',
|
main: '#222222',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
main: '#ef9a9a',
|
main: '#ef9a9a',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
default: '#121212',
|
default: '#121212',
|
||||||
paper: '#28282B',
|
paper: '#28282B',
|
||||||
},
|
},
|
||||||
backgroundSecondary: {
|
backgroundSecondary: {
|
||||||
default: '#0d0d0d',
|
default: '#0d0d0d',
|
||||||
paper: '#28282B',
|
paper: '#28282B',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: '#ffffff',
|
primary: '#ffffff',
|
||||||
secondary: '#000000',
|
secondary: '#000000',
|
||||||
},
|
},
|
||||||
textSecondaryDecorate: {
|
textSecondaryDecorate: {
|
||||||
main: '#6b6b6b',
|
main: '#6b6b6b',
|
||||||
light: '#000',
|
light: '#000',
|
||||||
dark: '#000',
|
dark: '#000',
|
||||||
contrastText: '#000',
|
contrastText: '#000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export { lightTheme, darkTheme }
|
export { lightTheme, darkTheme }
|
||||||
|
|||||||
@@ -20,103 +20,77 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
|
||||||
const AppPage = () => {
|
const AppPage = () => {
|
||||||
const { appNpub = '', npub = '' } = useParams()
|
const { appNpub = '', npub = '' } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const perms = useAppSelector((state) =>
|
const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
|
||||||
selectPermsByNpubAndAppNpub(state, npub, appNpub),
|
const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
|
||||||
)
|
|
||||||
const currentApp = useAppSelector((state) =>
|
|
||||||
selectAppByAppNpub(state, appNpub),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { open, handleClose, handleShow } = useToggleConfirm()
|
const { open, handleClose, handleShow } = useToggleConfirm()
|
||||||
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
||||||
|
|
||||||
const connectPerm = perms.find(
|
const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||||
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!currentApp) {
|
if (!currentApp) {
|
||||||
return <Navigate to={`/key/${npub}`} />
|
return <Navigate to={`/key/${npub}`} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { icon = '', name = '' } = currentApp || {}
|
const { icon = '', name = '' } = currentApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appName = name || getShortenNpub(appNpub)
|
||||||
const { timestamp } = connectPerm || {}
|
const { timestamp } = connectPerm || {}
|
||||||
|
|
||||||
const connectedOn =
|
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
||||||
connectPerm && timestamp
|
|
||||||
? `Connected at ${formatTimestampDate(timestamp)}`
|
|
||||||
: 'Not connected'
|
|
||||||
|
|
||||||
const handleDeleteApp = async () => {
|
const handleDeleteApp = async () => {
|
||||||
try {
|
try {
|
||||||
await swicCall('deleteApp', appNpub)
|
await swicCall('deleteApp', appNpub)
|
||||||
notify(`App: «${appName}» successfully deleted!`, 'success')
|
notify(`App: «${appName}» successfully deleted!`, 'success')
|
||||||
navigate(`/key/${npub}`)
|
navigate(`/key/${npub}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error?.message || 'Failed to delete app', 'error')
|
notify(error?.message || 'Failed to delete app', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack
|
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
||||||
maxHeight={'100%'}
|
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||||
overflow={'auto'}
|
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
|
||||||
alignItems={'flex-start'}
|
<StyledAppIcon src={icon} />
|
||||||
height={'100%'}
|
<Box flex={'1'} overflow={'hidden'}>
|
||||||
>
|
<Typography variant="h4" noWrap>
|
||||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
{appName}
|
||||||
<Stack
|
</Typography>
|
||||||
marginBottom={'1rem'}
|
<Typography variant="body2" noWrap>
|
||||||
direction={'row'}
|
{connectedOn}
|
||||||
gap={'1rem'}
|
</Typography>
|
||||||
width={'100%'}
|
</Box>
|
||||||
>
|
</Stack>
|
||||||
<StyledAppIcon src={icon} />
|
<Box marginBottom={'1rem'}>
|
||||||
<Box flex={'1'} overflow={'hidden'}>
|
<SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
|
||||||
<Typography variant='h4' noWrap>
|
<Button fullWidth onClick={handleShow}>
|
||||||
{appName}
|
Delete app
|
||||||
</Typography>
|
</Button>
|
||||||
<Typography variant='body2' noWrap>
|
</Box>
|
||||||
{connectedOn}
|
<Permissions perms={perms} />
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<Box marginBottom={'1rem'}>
|
|
||||||
<SectionTitle marginBottom={'0.5rem'}>
|
|
||||||
Disconnect
|
|
||||||
</SectionTitle>
|
|
||||||
<Button fullWidth onClick={handleShow}>
|
|
||||||
Delete app
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Permissions perms={perms} />
|
|
||||||
|
|
||||||
<Button
|
<Button fullWidth onClick={() => handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)}>
|
||||||
fullWidth
|
Activity
|
||||||
onClick={() =>
|
</Button>
|
||||||
handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)
|
</Stack>
|
||||||
}
|
|
||||||
>
|
|
||||||
Activity
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={open}
|
open={open}
|
||||||
headingText='Delete app'
|
headingText="Delete app"
|
||||||
description='Are you sure you want to delete this app?'
|
description="Are you sure you want to delete this app?"
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
onConfirm={handleDeleteApp}
|
onConfirm={handleDeleteApp}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
<ModalActivities appNpub={appNpub} />
|
<ModalActivities appNpub={appNpub} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppPage
|
export default AppPage
|
||||||
|
|||||||
@@ -10,36 +10,19 @@ import { ACTIONS } from '@/utils/consts'
|
|||||||
|
|
||||||
type ItemActivityProps = DbHistory
|
type ItemActivityProps = DbHistory
|
||||||
|
|
||||||
export const ItemActivity: FC<ItemActivityProps> = ({
|
export const ItemActivity: FC<ItemActivityProps> = ({ allowed, method, timestamp }) => {
|
||||||
allowed,
|
return (
|
||||||
method,
|
<StyledActivityItem>
|
||||||
timestamp,
|
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||||
}) => {
|
<Typography flex={1} fontWeight={700}>
|
||||||
return (
|
{ACTIONS[method] || method}
|
||||||
<StyledActivityItem>
|
</Typography>
|
||||||
<Box
|
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||||
display={'flex'}
|
</Box>
|
||||||
flexDirection={'column'}
|
<Box>{allowed ? <DoneRoundedIcon htmlColor="green" /> : <ClearRoundedIcon htmlColor="red" />}</Box>
|
||||||
gap={'0.5rem'}
|
<IconButton>
|
||||||
flex={1}
|
<MoreVertRoundedIcon />
|
||||||
>
|
</IconButton>
|
||||||
<Typography flex={1} fontWeight={700}>
|
</StyledActivityItem>
|
||||||
{ACTIONS[method] || method}
|
)
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2'>
|
|
||||||
{formatTimestampDate(timestamp)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
{allowed ? (
|
|
||||||
<DoneRoundedIcon htmlColor='green' />
|
|
||||||
) : (
|
|
||||||
<ClearRoundedIcon htmlColor='red' />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<IconButton>
|
|
||||||
<MoreVertRoundedIcon />
|
|
||||||
</IconButton>
|
|
||||||
</StyledActivityItem>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,23 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
|
||||||
type ModalActivitiesProps = {
|
type ModalActivitiesProps = {
|
||||||
appNpub: string
|
appNpub: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModalActivities: FC<ModalActivitiesProps> = ({ appNpub }) => {
|
export const ModalActivities: FC<ModalActivitiesProps> = ({ appNpub }) => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
|
|
||||||
const history = useLiveQuery(
|
const history = useLiveQuery(getActivityHistoryQuerier(appNpub), [], HistoryDefaultValue)
|
||||||
getActivityHistoryQuerier(appNpub),
|
|
||||||
[],
|
|
||||||
HistoryDefaultValue,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal open={isModalOpened} onClose={handleCloseModal} fixedHeight="calc(100% - 5rem)" title="Activity history">
|
||||||
open={isModalOpened}
|
<Box overflow={'auto'}>
|
||||||
onClose={handleCloseModal}
|
{history.map((item) => {
|
||||||
fixedHeight='calc(100% - 5rem)'
|
return <ItemActivity {...item} key={item.id} />
|
||||||
title='Activity history'
|
})}
|
||||||
>
|
</Box>
|
||||||
<Box overflow={'auto'}>
|
</Modal>
|
||||||
{history.map((item) => {
|
)
|
||||||
return <ItemActivity {...item} key={item.id} />
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import { Box, BoxProps } from '@mui/material'
|
import { Box, BoxProps } from '@mui/material'
|
||||||
|
|
||||||
export const StyledActivityItem = styled((props: BoxProps) => (
|
export const StyledActivityItem = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||||
<Box {...props} />
|
display: 'flex',
|
||||||
))(() => ({
|
gap: '0.5rem',
|
||||||
display: 'flex',
|
justifyContent: 'space-between',
|
||||||
gap: '0.5rem',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
padding: '0.25rem',
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0.25rem',
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -11,49 +11,31 @@ import { ItemPermissionMenu } from './ItemPermissionMenu'
|
|||||||
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
||||||
|
|
||||||
type ItemPermissionProps = {
|
type ItemPermissionProps = {
|
||||||
permission: DbPerm
|
permission: DbPerm
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||||
const { perm, value, timestamp, id } = permission || {}
|
const { perm, value, timestamp, id } = permission || {}
|
||||||
|
|
||||||
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
||||||
|
|
||||||
const isAllowed = value === '1'
|
const isAllowed = value === '1'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledPermissionItem>
|
<StyledPermissionItem>
|
||||||
<Box
|
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||||
display={'flex'}
|
<Typography flex={1} fontWeight={700}>
|
||||||
flexDirection={'column'}
|
{ACTIONS[perm] || perm}
|
||||||
gap={'0.5rem'}
|
</Typography>
|
||||||
flex={1}
|
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||||
>
|
</Box>
|
||||||
<Typography flex={1} fontWeight={700}>
|
<Box>{isAllowed ? <DoneRoundedIcon htmlColor="green" /> : <ClearRoundedIcon htmlColor="red" />}</Box>
|
||||||
{ACTIONS[perm] || perm}
|
<IconButton onClick={handleOpen}>
|
||||||
</Typography>
|
<MoreVertRoundedIcon />
|
||||||
<Typography variant='body2'>
|
</IconButton>
|
||||||
{formatTimestampDate(timestamp)}
|
</StyledPermissionItem>
|
||||||
</Typography>
|
<ItemPermissionMenu anchorEl={anchorEl} open={open} handleClose={handleClose} permId={id} />
|
||||||
</Box>
|
</>
|
||||||
<Box>
|
)
|
||||||
{isAllowed ? (
|
|
||||||
<DoneRoundedIcon htmlColor='green' />
|
|
||||||
) : (
|
|
||||||
<ClearRoundedIcon htmlColor='red' />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<IconButton onClick={handleOpen}>
|
|
||||||
<MoreVertRoundedIcon />
|
|
||||||
</IconButton>
|
|
||||||
</StyledPermissionItem>
|
|
||||||
<ItemPermissionMenu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={open}
|
|
||||||
handleClose={handleClose}
|
|
||||||
permId={id}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,58 +5,51 @@ import { swicCall } from '@/modules/swic'
|
|||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
|
||||||
type ItemPermissionMenuProps = {
|
type ItemPermissionMenuProps = {
|
||||||
permId: string
|
permId: string
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
} & MenuProps
|
} & MenuProps
|
||||||
|
|
||||||
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
|
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({ open, anchorEl, handleClose, permId }) => {
|
||||||
open,
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
anchorEl,
|
const notify = useEnqueueSnackbar()
|
||||||
handleClose,
|
|
||||||
permId,
|
|
||||||
}) => {
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
|
||||||
const notify = useEnqueueSnackbar()
|
|
||||||
|
|
||||||
const handleShowConfirm = () => {
|
const handleShowConfirm = () => {
|
||||||
setShowConfirm(true)
|
setShowConfirm(true)
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
const handleCloseConfirm = () => setShowConfirm(false)
|
const handleCloseConfirm = () => setShowConfirm(false)
|
||||||
|
|
||||||
const handleDeletePerm = async () => {
|
const handleDeletePerm = async () => {
|
||||||
try {
|
try {
|
||||||
await swicCall('deletePerm', permId)
|
await swicCall('deletePerm', permId)
|
||||||
notify('Permission successfully deleted!', 'success')
|
notify('Permission successfully deleted!', 'success')
|
||||||
handleCloseConfirm()
|
handleCloseConfirm()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error?.message || 'Failed to delete permission', 'error')
|
notify(error?.message || 'Failed to delete permission', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu
|
<Menu
|
||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
horizontal: 'left',
|
horizontal: 'left',
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleShowConfirm}>
|
<MenuItem onClick={handleShowConfirm}>Delete permission</MenuItem>
|
||||||
Delete permission
|
</Menu>
|
||||||
</MenuItem>
|
<ConfirmModal
|
||||||
</Menu>
|
open={showConfirm}
|
||||||
<ConfirmModal
|
onClose={handleCloseConfirm}
|
||||||
open={showConfirm}
|
onCancel={handleCloseConfirm}
|
||||||
onClose={handleCloseConfirm}
|
headingText="Delete permission"
|
||||||
onCancel={handleCloseConfirm}
|
description="Are you sure you want to delete this permission?"
|
||||||
headingText='Delete permission'
|
onConfirm={handleDeletePerm}
|
||||||
description='Are you sure you want to delete this permission?'
|
/>
|
||||||
onConfirm={handleDeletePerm}
|
</>
|
||||||
/>
|
)
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,18 @@ import { Box } from '@mui/material'
|
|||||||
import { ItemPermission } from './ItemPermission'
|
import { ItemPermission } from './ItemPermission'
|
||||||
|
|
||||||
type PermissionsProps = {
|
type PermissionsProps = {
|
||||||
perms: DbPerm[]
|
perms: DbPerm[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Permissions: FC<PermissionsProps> = ({ perms }) => {
|
export const Permissions: FC<PermissionsProps> = ({ perms }) => {
|
||||||
return (
|
return (
|
||||||
<Box width={'100%'} marginBottom={'1rem'} flex={1} overflow={'auto'}>
|
<Box width={'100%'} marginBottom={'1rem'} flex={1} overflow={'auto'}>
|
||||||
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
|
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
|
||||||
<Box
|
<Box flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={'0.5rem'}>
|
||||||
flex={1}
|
{perms.map((perm) => {
|
||||||
overflow={'auto'}
|
return <ItemPermission key={perm.id} permission={perm} />
|
||||||
display={'flex'}
|
})}
|
||||||
flexDirection={'column'}
|
</Box>
|
||||||
gap={'0.5rem'}
|
</Box>
|
||||||
>
|
)
|
||||||
{perms.map((perm) => {
|
|
||||||
return <ItemPermission key={perm.id} permission={perm} />
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Box, BoxProps, styled } from '@mui/material'
|
import { Box, BoxProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledPermissionItem = styled((props: BoxProps) => (
|
export const StyledPermissionItem = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||||
<Box {...props} />
|
display: 'flex',
|
||||||
))(() => ({
|
gap: '0.5rem',
|
||||||
display: 'flex',
|
justifyContent: 'space-between',
|
||||||
gap: '0.5rem',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
padding: '0.5rem',
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0.5rem',
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { Button } from '@/shared/Button/Button'
|
|||||||
import { styled } from '@mui/material'
|
import { styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledButton = styled(Button)({
|
export const StyledButton = styled(Button)({
|
||||||
textTransform: 'capitalize',
|
textTransform: 'capitalize',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Avatar, AvatarProps, styled } from '@mui/material'
|
import { Avatar, AvatarProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledAppIcon = styled((props: AvatarProps) => (
|
export const StyledAppIcon = styled((props: AvatarProps) => <Avatar {...props} variant="rounded" />)(() => ({
|
||||||
<Avatar {...props} variant='rounded' />
|
width: 70,
|
||||||
))(() => ({
|
height: 70,
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { DbHistory, db } from '@/modules/db'
|
import { DbHistory, db } from '@/modules/db'
|
||||||
|
|
||||||
export const getActivityHistoryQuerier = (appNpub: string) => () => {
|
export const getActivityHistoryQuerier = (appNpub: string) => () => {
|
||||||
if (!appNpub.trim().length) return []
|
if (!appNpub.trim().length) return []
|
||||||
|
|
||||||
const result = db.history
|
const result = db.history
|
||||||
.where('appNpub')
|
.where('appNpub')
|
||||||
.equals(appNpub)
|
.equals(appNpub)
|
||||||
.reverse()
|
.reverse()
|
||||||
.sortBy('timestamp')
|
.sortBy('timestamp')
|
||||||
.then(a => a.slice(0, 30))
|
.then((a) => a.slice(0, 30))
|
||||||
// .limit(30)
|
// .limit(30)
|
||||||
// .toArray()
|
// .toArray()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistoryDefaultValue: DbHistory[] = []
|
export const HistoryDefaultValue: DbHistory[] = []
|
||||||
|
|||||||
@@ -7,76 +7,67 @@ import { CheckmarkIcon } from '@/assets'
|
|||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
|
||||||
const AuthPage = () => {
|
const AuthPage = () => {
|
||||||
const isMobile = useMediaQuery('(max-width:600px)')
|
const isMobile = useMediaQuery('(max-width:600px)')
|
||||||
|
|
||||||
const [enteredValue, setEnteredValue] = useState('')
|
const [enteredValue, setEnteredValue] = useState('')
|
||||||
|
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setEnteredValue(e.target.value)
|
setEnteredValue(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAvailable = enteredValue.trim().length > 2
|
const isAvailable = enteredValue.trim().length > 2
|
||||||
|
|
||||||
const inputHelperText = isAvailable ? (
|
const inputHelperText = isAvailable ? (
|
||||||
<>
|
<>
|
||||||
<CheckmarkIcon /> Available
|
<CheckmarkIcon /> Available
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Don't worry, username can be changed later."
|
"Don't worry, username can be changed later."
|
||||||
)
|
)
|
||||||
|
|
||||||
const mainContent = (
|
const mainContent = (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
label='Enter a Username'
|
label="Enter a Username"
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder='Username'
|
placeholder="Username"
|
||||||
helperText={inputHelperText}
|
helperText={inputHelperText}
|
||||||
endAdornment={
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>
|
onChange={handleInputChange}
|
||||||
}
|
value={enteredValue}
|
||||||
onChange={handleInputChange}
|
helperTextProps={{
|
||||||
value={enteredValue}
|
sx: {
|
||||||
helperTextProps={{
|
'&.helper_text': {
|
||||||
sx: {
|
color: isAvailable ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
|
||||||
'&.helper_text': {
|
},
|
||||||
color: isAvailable
|
},
|
||||||
? theme.palette.success.main
|
}}
|
||||||
: theme.palette.textSecondaryDecorate.main,
|
/>
|
||||||
},
|
<Button fullWidth>Sign up</Button>
|
||||||
},
|
</>
|
||||||
}}
|
)
|
||||||
/>
|
|
||||||
<Button fullWidth>Sign up</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack height={'100%'} position={'relative'}>
|
<Stack height={'100%'} position={'relative'}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<StyledContent>
|
<StyledContent>
|
||||||
<Stack
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||||
direction={'row'}
|
<StyledAppLogo />
|
||||||
gap={'1rem'}
|
<Typography fontWeight={600} variant="h5">
|
||||||
alignItems={'center'}
|
Sign up
|
||||||
alignSelf={'flex-start'}
|
</Typography>
|
||||||
>
|
</Stack>
|
||||||
<StyledAppLogo />
|
{mainContent}
|
||||||
<Typography fontWeight={600} variant='h5'>
|
</StyledContent>
|
||||||
Sign up
|
) : (
|
||||||
</Typography>
|
<Stack gap={'1rem'} alignItems={'center'}>
|
||||||
</Stack>
|
{mainContent}
|
||||||
{mainContent}
|
</Stack>
|
||||||
</StyledContent>
|
)}
|
||||||
) : (
|
</Stack>
|
||||||
<Stack gap={'1rem'} alignItems={'center'}>
|
)
|
||||||
{mainContent}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuthPage
|
export default AuthPage
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { AppLogo } from '@/assets'
|
import { AppLogo } from '@/assets'
|
||||||
import { Stack, styled, StackProps, Box } from '@mui/material'
|
import { Stack, styled, StackProps, Box } from '@mui/material'
|
||||||
|
|
||||||
export const StyledContent = styled((props: StackProps) => (
|
export const StyledContent = styled((props: StackProps) => <Stack {...props} gap={'1rem'} alignItems={'center'} />)(({
|
||||||
<Stack {...props} gap={'1rem'} alignItems={'center'} />
|
theme,
|
||||||
))(({ theme }) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
background: theme.palette.secondary.main,
|
background: theme.palette.secondary.main,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '-1rem',
|
bottom: '-1rem',
|
||||||
left: '-1rem',
|
left: '-1rem',
|
||||||
width: 'calc(100% + 2rem)',
|
width: 'calc(100% + 2rem)',
|
||||||
height: '70%',
|
height: '70%',
|
||||||
borderTopLeftRadius: '2rem',
|
borderTopLeftRadius: '2rem',
|
||||||
borderTopRightRadius: '2rem',
|
borderTopRightRadius: '2rem',
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
maxWidth: '50rem',
|
maxWidth: '50rem',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledAppLogo = styled((props) => (
|
export const StyledAppLogo = styled((props) => (
|
||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#00000054',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const ConfirmPage = () => {
|
const ConfirmPage = () => {
|
||||||
return <div>ConfirmPage</div>
|
return <div>ConfirmPage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConfirmPage
|
export default ConfirmPage
|
||||||
|
|||||||
@@ -10,61 +10,47 @@ import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
|||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
const isNoKeys = !keys || keys.length === 0
|
const isNoKeys = !keys || keys.length === 0
|
||||||
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const handleLearnMore = () => {
|
const handleLearnMore = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.open(`https://info.${DOMAIN}`, '_blank').focus();
|
window.open(`https://info.${DOMAIN}`, '_blank').focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||||
<SectionTitle marginBottom={'0.5rem'}>
|
<SectionTitle marginBottom={'0.5rem'}>{isNoKeys ? 'Welcome' : 'Accounts:'}</SectionTitle>
|
||||||
{isNoKeys ? 'Welcome' : 'Accounts:'}
|
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||||
</SectionTitle>
|
{isNoKeys && (
|
||||||
<Stack gap={'0.5rem'} overflow={'auto'}>
|
<>
|
||||||
{isNoKeys && (
|
<Typography textAlign={'left'} variant="h6" paddingTop="1em">
|
||||||
<>
|
Nsec.app is a novel key storage app for Nostr.
|
||||||
<Typography textAlign={'left'} variant='h6' paddingTop='1em'>
|
</Typography>
|
||||||
Nsec.app is a novel key storage app for Nostr.
|
<GetStartedButton onClick={handleClickAddAccount}>Get started</GetStartedButton>
|
||||||
</Typography>
|
<Typography textAlign={'left'} variant="h6" paddingTop="2em">
|
||||||
<GetStartedButton onClick={handleClickAddAccount}>
|
Your keys are stored in your browser and can be used in many Nostr apps without the need for a browser
|
||||||
Get started
|
extension.
|
||||||
</GetStartedButton>
|
</Typography>
|
||||||
<Typography textAlign={'left'} variant='h6' paddingTop='2em'>
|
<LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
|
||||||
Your keys are stored in your browser and
|
</>
|
||||||
can be used in many Nostr apps without the
|
)}
|
||||||
need for a browser extension.
|
{!isNoKeys && (
|
||||||
</Typography>
|
<Fragment>
|
||||||
<LearnMoreButton onClick={handleLearnMore}>
|
<Box flex={1} overflow={'auto'} borderRadius={'8px'} padding={'0.25rem'}>
|
||||||
Learn more
|
{keys.map((key) => (
|
||||||
</LearnMoreButton>
|
<ItemKey {...key} key={key.npub} />
|
||||||
</>
|
))}
|
||||||
)}
|
</Box>
|
||||||
{!isNoKeys && (
|
<AddAccountButton onClick={handleClickAddAccount}>Add account</AddAccountButton>
|
||||||
<Fragment>
|
</Fragment>
|
||||||
<Box
|
)}
|
||||||
flex={1}
|
</Stack>
|
||||||
overflow={'auto'}
|
</Stack>
|
||||||
borderRadius={'8px'}
|
)
|
||||||
padding={'0.25rem'}
|
|
||||||
>
|
|
||||||
{keys.map((key) => (
|
|
||||||
<ItemKey {...key} key={key.npub} />
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<AddAccountButton onClick={handleClickAddAccount}>
|
|
||||||
Add account
|
|
||||||
</AddAccountButton>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
||||||
|
|||||||
@@ -1,61 +1,50 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { DbKey } from '../../../modules/db'
|
import { DbKey } from '../../../modules/db'
|
||||||
import {
|
import { Avatar, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||||
Avatar,
|
|
||||||
Stack,
|
|
||||||
StackProps,
|
|
||||||
Typography,
|
|
||||||
TypographyProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
import { getShortenNpub } from '../../../utils/helpers/helpers'
|
import { getShortenNpub } from '../../../utils/helpers/helpers'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
type ItemKeyProps = DbKey
|
type ItemKeyProps = DbKey
|
||||||
|
|
||||||
export const ItemKey: FC<ItemKeyProps> = (props) => {
|
export const ItemKey: FC<ItemKeyProps> = (props) => {
|
||||||
const { npub, profile } = props
|
const { npub, profile } = props
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleNavigate = () => {
|
const handleNavigate = () => {
|
||||||
navigate('/key/' + npub)
|
navigate('/key/' + npub)
|
||||||
}
|
}
|
||||||
const { name = '', picture = '' } = profile?.info || {}
|
const { name = '', picture = '' } = profile?.info || {}
|
||||||
const userName = name || getShortenNpub(npub)
|
const userName = name || getShortenNpub(npub)
|
||||||
const userAvatar = picture || ''
|
const userAvatar = picture || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledKeyContainer onClick={handleNavigate}>
|
<StyledKeyContainer onClick={handleNavigate}>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap='1rem'>
|
<Stack direction={'row'} alignItems={'center'} gap="1rem">
|
||||||
<Avatar src={userAvatar} alt={userName} />
|
<Avatar src={userAvatar} alt={userName} />
|
||||||
<StyledText variant='body1'>{userName}</StyledText>
|
<StyledText variant="body1">{userName}</StyledText>
|
||||||
</Stack>
|
</Stack>
|
||||||
</StyledKeyContainer>
|
</StyledKeyContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledKeyContainer = styled((props: StackProps) => (
|
const StyledKeyContainer = styled((props: StackProps) => <Stack marginBottom={'0.5rem'} gap={'0.25rem'} {...props} />)(
|
||||||
<Stack marginBottom={'0.5rem'} gap={'0.25rem'} {...props} />
|
({ theme }) => {
|
||||||
))(({ theme }) => {
|
return {
|
||||||
return {
|
boxShadow:
|
||||||
boxShadow:
|
theme.palette.mode === 'dark' ? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)' : '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
|
||||||
theme.palette.mode === 'dark'
|
borderRadius: '12px',
|
||||||
? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)'
|
padding: '0.5rem 1rem',
|
||||||
: '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
|
background: theme.palette.background.paper,
|
||||||
borderRadius: '12px',
|
':hover': {
|
||||||
padding: '0.5rem 1rem',
|
background: `${theme.palette.background.paper}95`,
|
||||||
background: theme.palette.background.paper,
|
},
|
||||||
':hover': {
|
cursor: 'pointer',
|
||||||
background: `${theme.palette.background.paper}95`,
|
}
|
||||||
},
|
}
|
||||||
cursor: 'pointer',
|
)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const StyledText = styled((props: TypographyProps) => (
|
export const StyledText = styled((props: TypographyProps) => <Typography {...props} />)({
|
||||||
<Typography {...props} />
|
fontWeight: 500,
|
||||||
))({
|
width: '100%',
|
||||||
fontWeight: 500,
|
wordBreak: 'break-all',
|
||||||
width: '100%',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
|
|||||||
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
|
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
|
||||||
|
|
||||||
export const AddAccountButton = styled((props: AppButtonProps) => (
|
export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||||
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
||||||
))(() => ({
|
))(() => ({
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
padding: '0.35rem 1rem',
|
padding: '0.35rem 1rem',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const GetStartedButton = styled((props: AppButtonProps) => (
|
export const GetStartedButton = styled((props: AppButtonProps) => (
|
||||||
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
|
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
|
||||||
))(() => ({
|
))(() => ({
|
||||||
alignSelf: 'left',
|
alignSelf: 'left',
|
||||||
padding: '0.35rem 1rem',
|
padding: '0.35rem 1rem',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const LearnMoreButton = styled((props: AppButtonProps) => (
|
export const LearnMoreButton = styled((props: AppButtonProps) => (
|
||||||
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
|
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
|
||||||
))(() => ({
|
))(() => ({
|
||||||
alignSelf: 'left',
|
alignSelf: 'left',
|
||||||
padding: '0.35rem 1rem',
|
padding: '0.35rem 1rem',
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -21,84 +21,69 @@ import { checkNpubSyncQuerier } from './utils'
|
|||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
|
||||||
const KeyPage = () => {
|
const KeyPage = () => {
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
||||||
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
||||||
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
|
||||||
// const { userNameWithPrefix } = useProfile(npub)
|
// const { userNameWithPrefix } = useProfile(npub)
|
||||||
const { handleEnableBackground, showWarning, isEnabling } =
|
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
||||||
useBackgroundSigning()
|
|
||||||
|
|
||||||
const key = keys.find(k => k.npub === npub)
|
const key = keys.find((k) => k.npub === npub)
|
||||||
let username = ''
|
let username = ''
|
||||||
if (key?.name) {
|
if (key?.name) {
|
||||||
if (key.name.includes('@'))
|
if (key.name.includes('@')) username = key.name
|
||||||
username = key.name
|
else username = `${key?.name}@${DOMAIN}`
|
||||||
else
|
}
|
||||||
username = `${key?.name}@${DOMAIN}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredApps = apps.filter((a) => a.npub === npub)
|
const filteredApps = apps.filter((a) => a.npub === npub)
|
||||||
const { prepareEventPendings } = useTriggerConfirmModal(
|
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
||||||
npub,
|
|
||||||
pending,
|
|
||||||
perms,
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleOpenConnectAppModal = () =>
|
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
|
||||||
|
|
||||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack gap={'1rem'} height={'100%'}>
|
<Stack gap={'1rem'} height={'100%'}>
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<BackgroundSigningWarning
|
<BackgroundSigningWarning isEnabling={isEnabling} onEnableBackSigning={handleEnableBackground} />
|
||||||
isEnabling={isEnabling}
|
)}
|
||||||
onEnableBackSigning={handleEnableBackground}
|
<UserValueSection
|
||||||
/>
|
title="Your login"
|
||||||
)}
|
value={username}
|
||||||
<UserValueSection
|
copyValue={username}
|
||||||
title='Your login'
|
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||||
value={username}
|
/>
|
||||||
copyValue={username}
|
<UserValueSection
|
||||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
title="Your NPUB"
|
||||||
/>
|
value={npub}
|
||||||
<UserValueSection
|
copyValue={npub}
|
||||||
title='Your NPUB'
|
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||||
value={npub}
|
/>
|
||||||
copyValue={npub}
|
|
||||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack direction={'row'} gap={'0.75rem'}>
|
<Stack direction={'row'} gap={'0.75rem'}>
|
||||||
<StyledIconButton onClick={handleOpenConnectAppModal}>
|
<StyledIconButton onClick={handleOpenConnectAppModal}>
|
||||||
<ShareIcon />
|
<ShareIcon />
|
||||||
Connect app
|
Connect app
|
||||||
</StyledIconButton>
|
</StyledIconButton>
|
||||||
|
|
||||||
<StyledIconButton
|
<StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
|
||||||
bgcolor_variant='secondary'
|
<SettingsIcon />
|
||||||
onClick={handleOpenSettingsModal}
|
Settings
|
||||||
withBadge={!isSynced}
|
</StyledIconButton>
|
||||||
>
|
</Stack>
|
||||||
<SettingsIcon />
|
|
||||||
Settings
|
|
||||||
</StyledIconButton>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<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} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KeyPage
|
export default KeyPage
|
||||||
|
|||||||
@@ -11,57 +11,41 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
|||||||
import { ItemApp } from './ItemApp'
|
import { ItemApp } from './ItemApp'
|
||||||
|
|
||||||
type AppsProps = {
|
type AppsProps = {
|
||||||
apps: DbApp[]
|
apps: DbApp[]
|
||||||
npub: string
|
npub: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async function deletePerm(id: string) {
|
async function deletePerm(id: string) {
|
||||||
call(async () => {
|
call(async () => {
|
||||||
await swicCall('deletePerm', id)
|
await swicCall('deletePerm', id)
|
||||||
notify('Perm deleted!', 'success')
|
notify('Perm deleted!', 'success')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
||||||
flex={1}
|
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||||
marginBottom={'1rem'}
|
<SectionTitle>Connected apps</SectionTitle>
|
||||||
display={'flex'}
|
<AppLink title="Discover Apps" />
|
||||||
flexDirection={'column'}
|
</Stack>
|
||||||
overflow={'auto'}
|
{!apps.length && (
|
||||||
>
|
<StyledEmptyAppsBox>
|
||||||
<Stack
|
<Typography className="message" variant="h5" fontWeight={600} textAlign={'center'}>
|
||||||
direction={'row'}
|
No connected apps
|
||||||
alignItems={'center'}
|
</Typography>
|
||||||
justifyContent={'space-between'}
|
<Button>Discover Nostr Apps</Button>
|
||||||
marginBottom={'0.5rem'}
|
</StyledEmptyAppsBox>
|
||||||
>
|
)}
|
||||||
<SectionTitle>Connected apps</SectionTitle>
|
|
||||||
<AppLink title='Discover Apps' />
|
|
||||||
</Stack>
|
|
||||||
{!apps.length && (
|
|
||||||
<StyledEmptyAppsBox>
|
|
||||||
<Typography
|
|
||||||
className='message'
|
|
||||||
variant='h5'
|
|
||||||
fontWeight={600}
|
|
||||||
textAlign={'center'}
|
|
||||||
>
|
|
||||||
No connected apps
|
|
||||||
</Typography>
|
|
||||||
<Button>Discover Nostr Apps</Button>
|
|
||||||
</StyledEmptyAppsBox>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
|
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
|
||||||
{apps.map((a) => (
|
{apps.map((a) => (
|
||||||
<ItemApp {...a} key={a.appNpub} />
|
<ItemApp {...a} key={a.appNpub} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,20 @@ import { CircularProgress, Stack } from '@mui/material'
|
|||||||
import GppMaybeIcon from '@mui/icons-material/GppMaybe'
|
import GppMaybeIcon from '@mui/icons-material/GppMaybe'
|
||||||
|
|
||||||
type BackgroundSigningWarningProps = {
|
type BackgroundSigningWarningProps = {
|
||||||
isEnabling: boolean
|
isEnabling: boolean
|
||||||
onEnableBackSigning: () => void
|
onEnableBackSigning: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({
|
export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ isEnabling, onEnableBackSigning }) => {
|
||||||
isEnabling,
|
return (
|
||||||
onEnableBackSigning,
|
<Warning
|
||||||
}) => {
|
message={
|
||||||
return (
|
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
|
||||||
<Warning
|
Please enable push notifications {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
||||||
message={
|
</Stack>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
|
}
|
||||||
Please enable push notifications{' '}
|
Icon={<GppMaybeIcon htmlColor="white" />}
|
||||||
{isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
onClick={isEnabling ? undefined : onEnableBackSigning}
|
||||||
</Stack>
|
/>
|
||||||
}
|
)
|
||||||
Icon={<GppMaybeIcon htmlColor='white' />}
|
|
||||||
onClick={isEnabling ? undefined : onEnableBackSigning}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,35 +9,25 @@ import { StyledItemAppContainer } from './styled'
|
|||||||
type ItemAppProps = DbApp
|
type ItemAppProps = DbApp
|
||||||
|
|
||||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appName = name || getShortenNpub(appNpub)
|
||||||
return (
|
return (
|
||||||
<StyledItemAppContainer
|
<StyledItemAppContainer
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
gap={'0.5rem'}
|
gap={'0.5rem'}
|
||||||
padding={'0.5rem 0'}
|
padding={'0.5rem 0'}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/key/${npub}/app/${appNpub}`}
|
to={`/key/${npub}/app/${appNpub}`}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar variant="square" sx={{ width: 56, height: 56 }} src={icon} alt={name} />
|
||||||
variant='square'
|
<Stack>
|
||||||
sx={{ width: 56, height: 56 }}
|
<Typography noWrap display={'block'} variant="body2">
|
||||||
src={icon}
|
{appName}
|
||||||
alt={name}
|
</Typography>
|
||||||
/>
|
<Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
|
||||||
<Stack>
|
Basic actions
|
||||||
<Typography noWrap display={'block'} variant='body2'>
|
</Typography>
|
||||||
{appName}
|
</Stack>
|
||||||
</Typography>
|
</StyledItemAppContainer>
|
||||||
<Typography
|
)
|
||||||
noWrap
|
|
||||||
display={'block'}
|
|
||||||
variant='caption'
|
|
||||||
color={'GrayText'}
|
|
||||||
>
|
|
||||||
Basic actions
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</StyledItemAppContainer>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,48 +8,31 @@ import { StyledInput } from '../styled'
|
|||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
|
||||||
type UserValueSectionProps = {
|
type UserValueSectionProps = {
|
||||||
title: string
|
title: string
|
||||||
value: string
|
value: string
|
||||||
explanationType: EXPLANATION_MODAL_KEYS
|
explanationType: EXPLANATION_MODAL_KEYS
|
||||||
copyValue: string
|
copyValue: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserValueSection: FC<UserValueSectionProps> = ({
|
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
|
||||||
title,
|
const { handleOpen } = useModalSearchParams()
|
||||||
value,
|
|
||||||
explanationType,
|
|
||||||
copyValue,
|
|
||||||
}) => {
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
|
||||||
|
|
||||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||||
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
|
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
|
||||||
search: {
|
search: {
|
||||||
type,
|
type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack
|
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||||
direction={'row'}
|
<SectionTitle>{title}</SectionTitle>
|
||||||
alignItems={'center'}
|
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
||||||
justifyContent={'space-between'}
|
</Stack>
|
||||||
marginBottom={'0.5rem'}
|
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
|
||||||
>
|
</Box>
|
||||||
<SectionTitle>{title}</SectionTitle>
|
)
|
||||||
<AppLink
|
|
||||||
title='What is this?'
|
|
||||||
onClick={() => handleOpenExplanationModal(explanationType)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<StyledInput
|
|
||||||
value={value}
|
|
||||||
readOnly
|
|
||||||
endAdornment={<InputCopyButton value={copyValue} />}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserValueSection
|
export default UserValueSection
|
||||||
|
|||||||
@@ -3,42 +3,40 @@ import { Stack, StackProps, styled } from '@mui/material'
|
|||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
export const StyledInput = styled(
|
export const StyledInput = styled(
|
||||||
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className='input'
|
className="input"
|
||||||
containerProps={{
|
containerProps={{
|
||||||
className,
|
className,
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
)(({ theme }) => ({
|
)(({ theme }) => ({
|
||||||
'& > .input': {
|
'& > .input': {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
background: theme.palette.secondary.main,
|
background: theme.palette.secondary.main,
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
'& .adornment': {
|
'& .adornment': {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const StyledItemAppContainer = styled(
|
export const StyledItemAppContainer = styled(<C extends React.ElementType>(props: StackProps<C, { component?: C }>) => (
|
||||||
<C extends React.ElementType>(props: StackProps<C, { component?: C }>) => (
|
<Stack {...props} />
|
||||||
<Stack {...props} />
|
))(({ theme }) => ({
|
||||||
),
|
textDecoration: 'none',
|
||||||
)(({ theme }) => ({
|
boxShadow: 'none',
|
||||||
textDecoration: 'none',
|
color: theme.palette.text.primary,
|
||||||
boxShadow: 'none',
|
background: theme.palette.backgroundSecondary.default,
|
||||||
color: theme.palette.text.primary,
|
borderRadius: '12px',
|
||||||
background: theme.palette.backgroundSecondary.default,
|
padding: '0.5rem 1rem',
|
||||||
borderRadius: '12px',
|
':hover': {
|
||||||
padding: '0.5rem 1rem',
|
background: `${theme.palette.backgroundSecondary.default}95`,
|
||||||
':hover': {
|
},
|
||||||
background: `${theme.palette.backgroundSecondary.default}95`,
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -4,37 +4,34 @@ import { askNotificationPermission } from '@/utils/helpers/helpers'
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
export const useBackgroundSigning = () => {
|
export const useBackgroundSigning = () => {
|
||||||
const [showWarning, setShowWarning] = useState(false)
|
const [showWarning, setShowWarning] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const checkBackgroundSigning = useCallback(async () => {
|
const checkBackgroundSigning = useCallback(async () => {
|
||||||
if (!swr) return undefined
|
if (!swr) return undefined
|
||||||
const isBackgroundEnable = await swr.pushManager.getSubscription()
|
const isBackgroundEnable = await swr.pushManager.getSubscription()
|
||||||
setShowWarning(!isBackgroundEnable)
|
setShowWarning(!isBackgroundEnable)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleEnableBackground = useCallback(async () => {
|
const handleEnableBackground = useCallback(async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
await askNotificationPermission()
|
await askNotificationPermission()
|
||||||
const result = await swicCall('enablePush')
|
const result = await swicCall('enablePush')
|
||||||
if (!result) throw new Error('Failed to activate the push subscription')
|
if (!result) throw new Error('Failed to activate the push subscription')
|
||||||
notify('Push notifications enabled!', 'success')
|
notify('Push notifications enabled!', 'success')
|
||||||
setShowWarning(false)
|
setShowWarning(false)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(
|
notify(`Failed to enable push subscription: ${error}`, 'error')
|
||||||
`Failed to enable push subscription: ${error}`,
|
}
|
||||||
'error',
|
setIsLoading(false)
|
||||||
)
|
checkBackgroundSigning()
|
||||||
}
|
}, [notify, checkBackgroundSigning])
|
||||||
setIsLoading(false)
|
|
||||||
checkBackgroundSigning()
|
|
||||||
}, [notify, checkBackgroundSigning])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkBackgroundSigning()
|
checkBackgroundSigning()
|
||||||
}, [checkBackgroundSigning])
|
}, [checkBackgroundSigning])
|
||||||
|
|
||||||
return { showWarning, isEnabling: isLoading, handleEnableBackground }
|
return { showWarning, isEnabling: isLoading, handleEnableBackground }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,27 +5,27 @@ import { getProfileUsername } from '@/utils/helpers/helpers'
|
|||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
|
||||||
export const useProfile = (npub: string) => {
|
export const useProfile = (npub: string) => {
|
||||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||||
|
|
||||||
const userName = getProfileUsername(profile, npub)
|
const userName = getProfileUsername(profile, npub)
|
||||||
// FIXME use nip05?
|
// FIXME use nip05?
|
||||||
const userNameWithPrefix = userName + '@' + DOMAIN
|
const userNameWithPrefix = userName + '@' + DOMAIN
|
||||||
|
|
||||||
const loadProfile = useCallback(async () => {
|
const loadProfile = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetchProfile(npub)
|
const response = await fetchProfile(npub)
|
||||||
setProfile(response)
|
setProfile(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch profile:', error)
|
console.error('Failed to fetch profile:', error)
|
||||||
}
|
}
|
||||||
}, [npub])
|
}, [npub])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProfile()
|
loadProfile()
|
||||||
}, [loadProfile])
|
}, [loadProfile])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profile,
|
profile,
|
||||||
userNameWithPrefix,
|
userNameWithPrefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,139 +5,102 @@ import { ACTION_TYPE } from '@/utils/consts'
|
|||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export type IPendingsByAppNpub = {
|
export type IPendingsByAppNpub = {
|
||||||
[appNpub: string]: {
|
[appNpub: string]: {
|
||||||
pending: DbPending[]
|
pending: DbPending[]
|
||||||
isConnected: boolean
|
isConnected: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type IShownConfirmModals = {
|
type IShownConfirmModals = {
|
||||||
[reqId: string]: boolean
|
[reqId: string]: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTriggerConfirmModal = (
|
export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms: DbPerm[]) => {
|
||||||
npub: string,
|
const { handleOpen, getModalOpened } = useModalSearchParams()
|
||||||
pending: DbPending[],
|
|
||||||
perms: DbPerm[],
|
|
||||||
) => {
|
|
||||||
const { handleOpen, getModalOpened } = useModalSearchParams()
|
|
||||||
|
|
||||||
const isConfirmConnectModalOpened = getModalOpened(
|
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||||
)
|
|
||||||
const isConfirmEventModalOpened = getModalOpened(
|
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
||||||
const filteredPerms = perms.filter((p) => p.npub === npub)
|
const filteredPerms = perms.filter((p) => p.npub === npub)
|
||||||
|
|
||||||
const npubConnectPerms = filteredPerms.filter(
|
const npubConnectPerms = filteredPerms.filter((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||||
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
const excludeConnectPendings = filteredPendingReqs.filter((pr) => pr.method !== 'connect')
|
||||||
)
|
const connectPendings = filteredPendingReqs.filter((pr) => pr.method === 'connect')
|
||||||
const excludeConnectPendings = filteredPendingReqs.filter(
|
|
||||||
(pr) => pr.method !== 'connect',
|
|
||||||
)
|
|
||||||
const connectPendings = filteredPendingReqs.filter(
|
|
||||||
(pr) => pr.method === 'connect',
|
|
||||||
)
|
|
||||||
|
|
||||||
const prepareEventPendings =
|
const prepareEventPendings = excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
|
||||||
excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
|
const isConnected = npubConnectPerms.some((cp) => cp.appNpub === current.appNpub)
|
||||||
const isConnected = npubConnectPerms.some(
|
if (!acc[current.appNpub]) {
|
||||||
(cp) => cp.appNpub === current.appNpub,
|
acc[current.appNpub] = {
|
||||||
)
|
pending: [current],
|
||||||
if (!acc[current.appNpub]) {
|
isConnected,
|
||||||
acc[current.appNpub] = {
|
}
|
||||||
pending: [current],
|
return acc
|
||||||
isConnected,
|
}
|
||||||
}
|
acc[current.appNpub].pending.push(current)
|
||||||
return acc
|
acc[current.appNpub].isConnected = isConnected
|
||||||
}
|
return acc
|
||||||
acc[current.appNpub].pending.push(current)
|
}, {})
|
||||||
acc[current.appNpub].isConnected = isConnected
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const shownConnectModals = useRef<IShownConfirmModals>({})
|
const shownConnectModals = useRef<IShownConfirmModals>({})
|
||||||
const shownConfirmEventModals = useRef<IShownConfirmModals>({})
|
const shownConfirmEventModals = useRef<IShownConfirmModals>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
shownConnectModals.current = {}
|
shownConnectModals.current = {}
|
||||||
shownConfirmEventModals.current = {}
|
shownConfirmEventModals.current = {}
|
||||||
}
|
}
|
||||||
}, [npub, pending.length])
|
}, [npub, pending.length])
|
||||||
|
|
||||||
const handleOpenConfirmConnectModal = useCallback(() => {
|
const handleOpenConfirmConnectModal = useCallback(() => {
|
||||||
if (
|
if (!filteredPendingReqs.length || isConfirmEventModalOpened || isConfirmConnectModalOpened) return undefined
|
||||||
!filteredPendingReqs.length ||
|
|
||||||
isConfirmEventModalOpened ||
|
|
||||||
isConfirmConnectModalOpened
|
|
||||||
)
|
|
||||||
return undefined
|
|
||||||
|
|
||||||
for (let i = 0; i < connectPendings.length; i++) {
|
for (let i = 0; i < connectPendings.length; i++) {
|
||||||
const req = connectPendings[i]
|
const req = connectPendings[i]
|
||||||
if (shownConnectModals.current[req.id]) {
|
if (shownConnectModals.current[req.id]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
shownConnectModals.current[req.id] = true
|
shownConnectModals.current[req.id] = true
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
search: {
|
search: {
|
||||||
appNpub: req.appNpub,
|
appNpub: req.appNpub,
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [
|
}, [connectPendings, filteredPendingReqs.length, handleOpen, isConfirmEventModalOpened, isConfirmConnectModalOpened])
|
||||||
connectPendings,
|
|
||||||
filteredPendingReqs.length,
|
|
||||||
handleOpen,
|
|
||||||
isConfirmEventModalOpened,
|
|
||||||
isConfirmConnectModalOpened,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleOpenConfirmEventModal = useCallback(() => {
|
const handleOpenConfirmEventModal = useCallback(() => {
|
||||||
if (!filteredPendingReqs.length || connectPendings.length)
|
if (!filteredPendingReqs.length || connectPendings.length) return undefined
|
||||||
return undefined
|
|
||||||
|
|
||||||
for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) {
|
for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) {
|
||||||
const appNpub = Object.keys(prepareEventPendings)[i]
|
const appNpub = Object.keys(prepareEventPendings)[i]
|
||||||
|
|
||||||
if (
|
if (shownConfirmEventModals.current[appNpub] || !prepareEventPendings[appNpub].isConnected) {
|
||||||
shownConfirmEventModals.current[appNpub] ||
|
continue
|
||||||
!prepareEventPendings[appNpub].isConnected
|
}
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
shownConfirmEventModals.current[appNpub] = true
|
shownConfirmEventModals.current[appNpub] = true
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||||
search: {
|
search: {
|
||||||
appNpub,
|
appNpub,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [
|
}, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings])
|
||||||
connectPendings.length,
|
|
||||||
filteredPendingReqs.length,
|
|
||||||
handleOpen,
|
|
||||||
prepareEventPendings,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleOpenConfirmEventModal()
|
handleOpenConfirmEventModal()
|
||||||
}, [handleOpenConfirmEventModal])
|
}, [handleOpenConfirmEventModal])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleOpenConfirmConnectModal()
|
handleOpenConfirmConnectModal()
|
||||||
}, [handleOpenConfirmConnectModal])
|
}, [handleOpenConfirmConnectModal])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prepareEventPendings,
|
prepareEventPendings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,82 +3,76 @@ import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
|||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
type StyledIconButtonProps = ButtonProps & {
|
type StyledIconButtonProps = ButtonProps & {
|
||||||
bgcolor_variant?: 'primary' | 'secondary'
|
bgcolor_variant?: 'primary' | 'secondary'
|
||||||
withBadge?: boolean
|
withBadge?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledIconButton = styled(
|
export const StyledIconButton = styled(({ withBadge, ...props }: StyledIconButtonProps) => {
|
||||||
({ withBadge, ...props }: StyledIconButtonProps) => {
|
if (withBadge) {
|
||||||
if (withBadge) {
|
return (
|
||||||
return (
|
<Badge sx={{ flex: 1 }} badgeContent={''} color="error">
|
||||||
<Badge sx={{ flex: 1 }} badgeContent={''} color='error'>
|
<Button {...props} />
|
||||||
<Button {...props} />
|
</Badge>
|
||||||
</Badge>
|
)
|
||||||
)
|
}
|
||||||
}
|
return <Button {...props} />
|
||||||
return <Button {...props} />
|
})(({ bgcolor_variant = 'primary', theme }) => {
|
||||||
},
|
const isPrimary = bgcolor_variant === 'primary'
|
||||||
)(({ bgcolor_variant = 'primary', theme }) => {
|
return {
|
||||||
const isPrimary = bgcolor_variant === 'primary'
|
flex: '1',
|
||||||
return {
|
padding: '0.75rem',
|
||||||
flex: '1',
|
display: 'flex',
|
||||||
padding: '0.75rem',
|
flexDirection: 'column',
|
||||||
display: 'flex',
|
alignItems: 'flex-start',
|
||||||
flexDirection: 'column',
|
gap: '0.5rem',
|
||||||
alignItems: 'flex-start',
|
borderRadius: '1rem',
|
||||||
gap: '0.5rem',
|
fontSize: '0.875rem',
|
||||||
borderRadius: '1rem',
|
'&:is(:hover, :active, &)': {
|
||||||
fontSize: '0.875rem',
|
background: isPrimary ? theme.palette.primary.main : theme.palette.secondary.main,
|
||||||
'&:is(:hover, :active, &)': {
|
},
|
||||||
background: isPrimary
|
color: isPrimary ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||||
? theme.palette.primary.main
|
}
|
||||||
: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
color: isPrimary
|
|
||||||
? theme.palette.text.secondary
|
|
||||||
: theme.palette.text.primary,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
|
export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
|
||||||
return {
|
return {
|
||||||
minHeight: '186px',
|
minHeight: '186px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
background: theme.palette.secondary.main,
|
background: theme.palette.secondary.main,
|
||||||
borderRadius: '24px',
|
borderRadius: '24px',
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
'& > .message': {
|
'& > .message': {
|
||||||
flex: '1',
|
flex: '1',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
opacity: '0.6',
|
opacity: '0.6',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledInput = styled(
|
export const StyledInput = styled(
|
||||||
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className='input'
|
className="input"
|
||||||
containerProps={{
|
containerProps={{
|
||||||
className,
|
className,
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
)(({ theme }) => ({
|
)(({ theme }) => ({
|
||||||
'& > .input': {
|
'& > .input': {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
background: theme.palette.secondary.main,
|
background: theme.palette.secondary.main,
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
'& .adornment': {
|
'& .adornment': {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from '@/modules/db'
|
import { db } from '@/modules/db'
|
||||||
|
|
||||||
export const checkNpubSyncQuerier = (npub: string) => async () => {
|
export const checkNpubSyncQuerier = (npub: string) => async () => {
|
||||||
const count = await db.syncHistory.where('npub').equals(npub).count()
|
const count = await db.syncHistory.where('npub').equals(npub).count()
|
||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,91 +6,73 @@ import { Box, Button, Stack, TextField } from '@mui/material'
|
|||||||
import { useEnqueueSnackbar } from '../hooks/useEnqueueSnackbar'
|
import { useEnqueueSnackbar } from '../hooks/useEnqueueSnackbar'
|
||||||
|
|
||||||
const WelcomePage = () => {
|
const WelcomePage = () => {
|
||||||
const keys = useAppSelector((state) => state.content.keys)
|
const keys = useAppSelector((state) => state.content.keys)
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const isKeysExists = keys.length > 0
|
const isKeysExists = keys.length > 0
|
||||||
|
|
||||||
const nsecInputRef = useRef<HTMLInputElement | null>(null)
|
const nsecInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const npubInputRef = useRef<HTMLInputElement | null>(null)
|
const npubInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
if (isKeysExists) return <Navigate to={'/home'} />
|
if (isKeysExists) return <Navigate to={'/home'} />
|
||||||
|
|
||||||
async function generateKey() {
|
async function generateKey() {
|
||||||
try {
|
try {
|
||||||
const k: any = await swicCall('generateKey')
|
const k: any = await swicCall('generateKey')
|
||||||
notify(`New key ${k.npub}`, 'success')
|
notify(`New key ${k.npub}`, 'success')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importKey() {
|
async function importKey() {
|
||||||
try {
|
try {
|
||||||
const nsec = nsecInputRef.current?.value
|
const nsec = nsecInputRef.current?.value
|
||||||
if (!nsec) return
|
if (!nsec) return
|
||||||
await swicCall('importKey', nsec)
|
await swicCall('importKey', nsec)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNewKey() {
|
async function fetchNewKey() {
|
||||||
try {
|
try {
|
||||||
const npub = npubInputRef.current?.value
|
const npub = npubInputRef.current?.value
|
||||||
const passphrase = passwordInputRef.current?.value
|
const passphrase = passwordInputRef.current?.value
|
||||||
const k: any = await swicCall('fetchKey', npub, passphrase)
|
const k: any = await swicCall('fetchKey', npub, passphrase)
|
||||||
notify(`Fetched ${k.npub}`, 'success')
|
notify(`Fetched ${k.npub}`, 'success')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={'1.5rem'}>
|
<Stack gap={'1.5rem'}>
|
||||||
<Box alignSelf={'center'}>
|
<Box alignSelf={'center'}>
|
||||||
<Button size='small' variant='contained' onClick={generateKey}>
|
<Button size="small" variant="contained" onClick={generateKey}>
|
||||||
generate key
|
generate key
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack alignItems={'center'} gap='0.5rem'>
|
<Stack alignItems={'center'} gap="0.5rem">
|
||||||
<TextField
|
<TextField variant="outlined" ref={nsecInputRef} placeholder="Enter nsec..." fullWidth size="small" />
|
||||||
variant='outlined'
|
<Button size="small" variant="contained" onClick={importKey}>
|
||||||
ref={nsecInputRef}
|
import key (DANGER!)
|
||||||
placeholder='Enter nsec...'
|
</Button>
|
||||||
fullWidth
|
</Stack>
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
<Button size='small' variant='contained' onClick={importKey}>
|
|
||||||
import key (DANGER!)
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack alignItems={'center'} gap='0.5rem'>
|
<Stack alignItems={'center'} gap="0.5rem">
|
||||||
<Stack width={'100%'} gap='0.5rem'>
|
<Stack width={'100%'} gap="0.5rem">
|
||||||
<TextField
|
<TextField variant="outlined" ref={npubInputRef} placeholder="Enter npub..." fullWidth size="small" />
|
||||||
variant='outlined'
|
<TextField variant="outlined" ref={passwordInputRef} placeholder="Enter password" fullWidth size="small" />
|
||||||
ref={npubInputRef}
|
</Stack>
|
||||||
placeholder='Enter npub...'
|
<Button size="small" variant="contained" onClick={fetchNewKey}>
|
||||||
fullWidth
|
fetch key
|
||||||
size='small'
|
</Button>
|
||||||
/>
|
</Stack>
|
||||||
<TextField
|
</Stack>
|
||||||
variant='outlined'
|
)
|
||||||
ref={passwordInputRef}
|
|
||||||
placeholder='Enter password'
|
|
||||||
fullWidth
|
|
||||||
size='small'
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Button size='small' variant='contained' onClick={fetchNewKey}>
|
|
||||||
fetch key
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WelcomePage
|
export default WelcomePage
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ReportHandler } from 'web-vitals';
|
import { ReportHandler } from 'web-vitals'
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
getCLS(onPerfEntry);
|
getCLS(onPerfEntry)
|
||||||
getFID(onPerfEntry);
|
getFID(onPerfEntry)
|
||||||
getFCP(onPerfEntry);
|
getFCP(onPerfEntry)
|
||||||
getLCP(onPerfEntry);
|
getLCP(onPerfEntry)
|
||||||
getTTFB(onPerfEntry);
|
getTTFB(onPerfEntry)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default reportWebVitals;
|
export default reportWebVitals
|
||||||
|
|||||||
@@ -10,33 +10,27 @@ const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
|||||||
const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
|
const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
|
||||||
|
|
||||||
const LoadingSpinner = () => (
|
const LoadingSpinner = () => (
|
||||||
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
|
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<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='/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
|
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
|
||||||
path='/key/:npub/app/:appNpub'
|
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
|
||||||
element={<AppPage />}
|
</Route>
|
||||||
/>
|
<Route path="*" element={<Navigate to={'/home'} />} />
|
||||||
<Route
|
</Routes>
|
||||||
path='/key/:npub/:req_id'
|
</Suspense>
|
||||||
element={<ConfirmPage />}
|
)
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route path='*' element={<Navigate to={'/home'} />} />
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppRoutes
|
export default AppRoutes
|
||||||
|
|||||||
@@ -30,61 +30,60 @@ precacheAndRoute(self.__WB_MANIFEST)
|
|||||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
|
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
|
||||||
registerRoute(
|
registerRoute(
|
||||||
// Return false to exempt requests from being fulfilled by index.html.
|
// Return false to exempt requests from being fulfilled by index.html.
|
||||||
({ request, url }: { request: Request; url: URL }) => {
|
({ request, url }: { request: Request; url: URL }) => {
|
||||||
// If this isn't a navigation, skip.
|
// If this isn't a navigation, skip.
|
||||||
if (request.mode !== 'navigate') {
|
if (request.mode !== 'navigate') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a URL that starts with /_, skip.
|
// If this is a URL that starts with /_, skip.
|
||||||
if (url.pathname.startsWith('/_')) {
|
if (url.pathname.startsWith('/_')) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this looks like a URL for a resource, because it contains
|
// If this looks like a URL for a resource, because it contains
|
||||||
// a file extension, skip.
|
// a file extension, skip.
|
||||||
if (url.pathname.match(fileExtensionRegexp)) {
|
if (url.pathname.match(fileExtensionRegexp)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return true to signal that we want to use the handler.
|
// Return true to signal that we want to use the handler.
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'),
|
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||||
)
|
)
|
||||||
|
|
||||||
// An example runtime caching route for requests that aren't handled by the
|
// An example runtime caching route for requests that aren't handled by the
|
||||||
// precache, in this case same-origin .png requests like those from in public/
|
// precache, in this case same-origin .png requests like those from in public/
|
||||||
registerRoute(
|
registerRoute(
|
||||||
// Add in any other file extensions or routing criteria as needed.
|
// Add in any other file extensions or routing criteria as needed.
|
||||||
({ url }) =>
|
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
||||||
url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
// Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||||
// Customize this strategy as needed, e.g., by changing to CacheFirst.
|
new StaleWhileRevalidate({
|
||||||
new StaleWhileRevalidate({
|
cacheName: 'images',
|
||||||
cacheName: 'images',
|
plugins: [
|
||||||
plugins: [
|
// Ensure that once this runtime cache reaches a maximum size the
|
||||||
// Ensure that once this runtime cache reaches a maximum size the
|
// least-recently used images are removed.
|
||||||
// least-recently used images are removed.
|
new ExpirationPlugin({ maxEntries: 50 }),
|
||||||
new ExpirationPlugin({ maxEntries: 50 }),
|
],
|
||||||
],
|
})
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// This allows the web app to trigger skipWaiting via
|
// This allows the web app to trigger skipWaiting via
|
||||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||||
self.addEventListener('message', (event) => {
|
self.addEventListener('message', (event) => {
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
self.skipWaiting()
|
self.skipWaiting()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Any other custom service worker logic can go here.
|
// Any other custom service worker logic can go here.
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
console.log('worker starting')
|
console.log('worker starting')
|
||||||
const backend = new NoauthBackend(self)
|
const backend = new NoauthBackend(self)
|
||||||
await backend.start()
|
await backend.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
start()
|
start()
|
||||||
|
|||||||
@@ -16,34 +16,34 @@ const isLocalhost = Boolean(
|
|||||||
window.location.hostname === '[::1]' ||
|
window.location.hostname === '[::1]' ||
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||||
);
|
)
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
onSuccess?: (registration: ServiceWorkerRegistration) => void
|
||||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
onUpdate?: (registration: ServiceWorkerRegistration) => void
|
||||||
onError?: (e: any) => void;
|
onError?: (e: any) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
export function register(config?: Config) {
|
export function register(config?: Config) {
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
if (config && config.onError) {
|
if (config && config.onError) {
|
||||||
config.onError(new Error("Wrong origin"));
|
config.onError(new Error('Wrong origin'))
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||||
|
|
||||||
if (isLocalhost) {
|
if (isLocalhost) {
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
checkValidServiceWorker(swUrl, config);
|
checkValidServiceWorker(swUrl, config)
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
// service worker/PWA documentation.
|
// service worker/PWA documentation.
|
||||||
@@ -51,16 +51,16 @@ export function register(config?: Config) {
|
|||||||
console.log(
|
console.log(
|
||||||
'This web app is being served cache-first by a service ' +
|
'This web app is being served cache-first by a service ' +
|
||||||
'worker. To learn more, visit https://cra.link/PWA'
|
'worker. To learn more, visit https://cra.link/PWA'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Is not localhost. Just register service worker
|
// Is not localhost. Just register service worker
|
||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
if (config && config.onError) {
|
if (config && config.onError) {
|
||||||
config.onError(new Error("No service worker"));
|
config.onError(new Error('No service worker'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,9 +70,9 @@ function registerValidSW(swUrl: string, config?: Config) {
|
|||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing
|
||||||
if (installingWorker == null) {
|
if (installingWorker == null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = () => {
|
||||||
if (installingWorker.state === 'installed') {
|
if (installingWorker.state === 'installed') {
|
||||||
@@ -83,33 +83,33 @@ function registerValidSW(swUrl: string, config?: Config) {
|
|||||||
console.log(
|
console.log(
|
||||||
'New content is available and will be used when all ' +
|
'New content is available and will be used when all ' +
|
||||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||||
);
|
)
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onUpdate) {
|
if (config && config.onUpdate) {
|
||||||
config.onUpdate(registration);
|
config.onUpdate(registration)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// At this point, everything has been precached.
|
// At this point, everything has been precached.
|
||||||
// It's the perfect time to display a
|
// It's the perfect time to display a
|
||||||
// "Content is cached for offline use." message.
|
// "Content is cached for offline use." message.
|
||||||
console.log('Content is cached for offline use.');
|
console.log('Content is cached for offline use.')
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onSuccess) {
|
if (config && config.onSuccess) {
|
||||||
config.onSuccess(registration);
|
config.onSuccess(registration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error);
|
console.error('Error during service worker registration:', error)
|
||||||
if (config && config.onError) {
|
if (config && config.onError) {
|
||||||
config.onError(new Error(`Install error: ${error}`));
|
config.onError(new Error(`Install error: ${error}`))
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
@@ -119,35 +119,32 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type')
|
||||||
if (
|
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
|
||||||
response.status === 404 ||
|
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload();
|
window.location.reload()
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Service worker found. Proceed as normal.
|
// Service worker found. Proceed as normal.
|
||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log('No internet connection found. App is running in offline mode.');
|
console.log('No internet connection found. App is running in offline mode.')
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready
|
navigator.serviceWorker.ready
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error.message);
|
console.error(error.message)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom'
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import { Typography, TypographyProps, styled } from '@mui/material'
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
type AppLinkProps = {
|
type AppLinkProps = {
|
||||||
title: string
|
title: string
|
||||||
} & TypographyProps
|
} & TypographyProps
|
||||||
|
|
||||||
export const AppLink: FC<AppLinkProps> = ({ title = '', ...rest }) => {
|
export const AppLink: FC<AppLinkProps> = ({ title = '', ...rest }) => {
|
||||||
return <StyledTypography {...rest}>{title}</StyledTypography>
|
return <StyledTypography {...rest}>{title}</StyledTypography>
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledTypography = styled((props: TypographyProps) => (
|
const StyledTypography = styled((props: TypographyProps) => <Typography {...props} variant="caption" />)(({
|
||||||
<Typography {...props} variant='caption' />
|
theme,
|
||||||
))(({ theme }) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
color: theme.palette.textSecondaryDecorate.main,
|
color: theme.palette.textSecondaryDecorate.main,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
'&:active': {
|
'&:active': {
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,53 +1,45 @@
|
|||||||
import {
|
import { styled, Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material'
|
||||||
styled,
|
|
||||||
Button as MuiButton,
|
|
||||||
ButtonProps as MuiButtonProps,
|
|
||||||
} from '@mui/material'
|
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
export type AppButtonProps = MuiButtonProps & {
|
export type AppButtonProps = MuiButtonProps & {
|
||||||
varianttype?: 'light' | 'default' | 'dark' | 'secondary'
|
varianttype?: 'light' | 'default' | 'dark' | 'secondary'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(
|
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(({ children, ...restProps }, ref) => {
|
||||||
({ children, ...restProps }, ref) => {
|
return (
|
||||||
return (
|
<StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}>
|
||||||
<StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}>
|
{children}
|
||||||
{children}
|
</StyledButton>
|
||||||
</StyledButton>
|
)
|
||||||
)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const StyledButton = styled(
|
const StyledButton = styled(
|
||||||
forwardRef<HTMLButtonElement, AppButtonProps>((props, ref) => (
|
forwardRef<HTMLButtonElement, AppButtonProps>((props, ref) => <MuiButton ref={ref} {...props} />)
|
||||||
<MuiButton ref={ref} {...props} />
|
|
||||||
)),
|
|
||||||
)(({ theme, varianttype = 'default' }) => {
|
)(({ theme, varianttype = 'default' }) => {
|
||||||
const commonStyles = {
|
const commonStyles = {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
}
|
}
|
||||||
if (varianttype === 'secondary') {
|
if (varianttype === 'secondary') {
|
||||||
return {
|
return {
|
||||||
...commonStyles,
|
...commonStyles,
|
||||||
'&.button:is(:hover, :active, &)': {
|
'&.button:is(:hover, :active, &)': {
|
||||||
background: theme.palette.backgroundSecondary.default,
|
background: theme.palette.backgroundSecondary.default,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...commonStyles,
|
...commonStyles,
|
||||||
'&.button:is(:hover, :active, &)': {
|
'&.button:is(:hover, :active, &)': {
|
||||||
background: theme.palette.primary.main,
|
background: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
':disabled': {
|
':disabled': {
|
||||||
'&.button:is(:hover, :active, &)': {
|
'&.button:is(:hover, :active, &)': {
|
||||||
background: theme.palette.backgroundSecondary.default,
|
background: theme.palette.backgroundSecondary.default,
|
||||||
},
|
},
|
||||||
color: theme.palette.backgroundSecondary.paper,
|
color: theme.palette.backgroundSecondary.paper,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,38 +1,27 @@
|
|||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import { Checkbox as MuiCheckbox, CheckboxProps, styled } from '@mui/material'
|
import { Checkbox as MuiCheckbox, CheckboxProps, styled } from '@mui/material'
|
||||||
import {
|
import { CheckedIcon, CheckedLightIcon, UnchekedIcon, UnchekedLightIcon } from '@/assets'
|
||||||
CheckedIcon,
|
|
||||||
CheckedLightIcon,
|
|
||||||
UnchekedIcon,
|
|
||||||
UnchekedLightIcon,
|
|
||||||
} from '@/assets'
|
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
|
|
||||||
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>((props, ref) => {
|
||||||
(props, ref) => {
|
const { themeMode } = useAppSelector((state) => state.ui)
|
||||||
const { themeMode } = useAppSelector((state) => state.ui)
|
|
||||||
|
|
||||||
return <StyledCheckbox ref={ref} {...props} mode={themeMode} />
|
return <StyledCheckbox ref={ref} {...props} mode={themeMode} />
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const StyledCheckbox = styled(
|
const StyledCheckbox = styled(
|
||||||
forwardRef<HTMLButtonElement, CheckboxProps & { mode: 'dark' | 'light' }>(
|
forwardRef<HTMLButtonElement, CheckboxProps & { mode: 'dark' | 'light' }>(({ mode, ...restProps }, ref) => {
|
||||||
({ mode, ...restProps }, ref) => {
|
const isDarkMode = mode === 'dark'
|
||||||
const isDarkMode = mode === 'dark'
|
return (
|
||||||
return (
|
<MuiCheckbox
|
||||||
<MuiCheckbox
|
{...restProps}
|
||||||
{...restProps}
|
ref={ref}
|
||||||
ref={ref}
|
icon={isDarkMode ? <UnchekedLightIcon /> : <UnchekedIcon />}
|
||||||
icon={isDarkMode ? <UnchekedLightIcon /> : <UnchekedIcon />}
|
checkedIcon={isDarkMode ? <CheckedLightIcon /> : <CheckedIcon />}
|
||||||
checkedIcon={
|
/>
|
||||||
isDarkMode ? <CheckedLightIcon /> : <CheckedIcon />
|
)
|
||||||
}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)(() => ({
|
)(() => ({
|
||||||
'& .MuiSvgIcon-root': { fontSize: '1.5rem' },
|
'& .MuiSvgIcon-root': { fontSize: '1.5rem' },
|
||||||
marginLeft: '-10px',
|
marginLeft: '-10px',
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,65 +1,58 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
import {
|
import { Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, Slide } from '@mui/material'
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogProps,
|
|
||||||
DialogTitle,
|
|
||||||
Slide,
|
|
||||||
} from '@mui/material'
|
|
||||||
import { Button } from '../Button/Button'
|
import { Button } from '../Button/Button'
|
||||||
import { TransitionProps } from '@mui/material/transitions'
|
import { TransitionProps } from '@mui/material/transitions'
|
||||||
import { StyledDialogContentText } from './styled'
|
import { StyledDialogContentText } from './styled'
|
||||||
|
|
||||||
const Transition = React.forwardRef(function Transition(
|
const Transition = React.forwardRef(function Transition(
|
||||||
props: TransitionProps & {
|
props: TransitionProps & {
|
||||||
children: React.ReactElement<any, any>
|
children: React.ReactElement<any, any>
|
||||||
},
|
},
|
||||||
ref: React.Ref<unknown>,
|
ref: React.Ref<unknown>
|
||||||
) {
|
) {
|
||||||
return <Slide direction='up' ref={ref} {...props} />
|
return <Slide direction="up" ref={ref} {...props} />
|
||||||
})
|
})
|
||||||
|
|
||||||
type ConfirmModalProps = {
|
type ConfirmModalProps = {
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
headingText: string
|
headingText: string
|
||||||
description?: string
|
description?: string
|
||||||
} & DialogProps
|
} & DialogProps
|
||||||
|
|
||||||
export const ConfirmModal: FC<ConfirmModalProps> = ({
|
export const ConfirmModal: FC<ConfirmModalProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
headingText = 'Confirm',
|
headingText = 'Confirm',
|
||||||
description,
|
description,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
TransitionComponent={Transition}
|
TransitionComponent={Transition}
|
||||||
keepMounted
|
keepMounted
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
sx={{ zIndex: 1302 }}
|
sx={{ zIndex: 1302 }}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: {
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle fontWeight={600} fontSize={'1.5rem'}>
|
<DialogTitle fontWeight={600} fontSize={'1.5rem'}>
|
||||||
{headingText}
|
{headingText}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<StyledDialogContentText>{description}</StyledDialogContentText>
|
<StyledDialogContentText>{description}</StyledDialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button varianttype='secondary' onClick={onCancel}>
|
<Button varianttype="secondary" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onConfirm}>Confirm</Button>
|
<Button onClick={onConfirm}>Confirm</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import {
|
import { DialogContentText, DialogContentTextProps, styled } from '@mui/material'
|
||||||
DialogContentText,
|
|
||||||
DialogContentTextProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export const StyledDialogContentText = styled(
|
export const StyledDialogContentText = styled((props: DialogContentTextProps) => <DialogContentText {...props} />)(
|
||||||
(props: DialogContentTextProps) => <DialogContentText {...props} />,
|
({ theme }) => ({
|
||||||
)(({ theme }) => ({
|
color: theme.palette.primary.main,
|
||||||
color: theme.palette.primary.main,
|
})
|
||||||
}))
|
)
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import { forwardRef, useRef } from "react";
|
import { forwardRef, useRef } from 'react'
|
||||||
import { Input, InputProps } from "../Input/Input";
|
import { Input, InputProps } from '../Input/Input'
|
||||||
|
|
||||||
export type DebounceProps = {
|
export type DebounceProps = {
|
||||||
handleDebounce: (value: string) => void;
|
handleDebounce: (value: string) => void
|
||||||
debounceTimeout: number;
|
debounceTimeout: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export const DebounceInput = (props: InputProps & DebounceProps) => {
|
export const DebounceInput = (props: InputProps & DebounceProps) => {
|
||||||
const { handleDebounce, debounceTimeout, ...rest } = props;
|
const { handleDebounce, debounceTimeout, ...rest } = props
|
||||||
|
|
||||||
const timerRef = useRef<number>();
|
const timerRef = useRef<number>()
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
timerRef.current = window.setTimeout(() => {
|
timerRef.current = window.setTimeout(() => {
|
||||||
handleDebounce(event.target.value);
|
handleDebounce(event.target.value)
|
||||||
}, debounceTimeout);
|
}, debounceTimeout)
|
||||||
};
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return <Input {...rest} onChange={handleChange} />;
|
return <Input {...rest} onChange={handleChange} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ import useIsIOS from '@/hooks/useIsIOS'
|
|||||||
import { StyledButton } from './styled'
|
import { StyledButton } from './styled'
|
||||||
|
|
||||||
type IOSBackButtonProps = ButtonProps & {
|
type IOSBackButtonProps = ButtonProps & {
|
||||||
onNavigate?: () => void
|
onNavigate?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IOSBackButton: FC<IOSBackButtonProps> = ({ onNavigate }) => {
|
export const IOSBackButton: FC<IOSBackButtonProps> = ({ onNavigate }) => {
|
||||||
const isIOS = useIsIOS()
|
const isIOS = useIsIOS()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleNavigateBack = () => {
|
const handleNavigateBack = () => {
|
||||||
if (onNavigate && typeof onNavigate === 'function') {
|
if (onNavigate && typeof onNavigate === 'function') {
|
||||||
return onNavigate()
|
return onNavigate()
|
||||||
}
|
}
|
||||||
navigate(-1)
|
navigate(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isIOS) return null
|
if (!isIOS) return null
|
||||||
|
|
||||||
return <StyledButton onClick={handleNavigateBack}>Back</StyledButton>
|
return <StyledButton onClick={handleNavigateBack}>Back</StyledButton>
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user