chore(git): merge branch 'staging' into 175-local-sigit-draft
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 55s

This commit is contained in:
en 2025-03-06 17:41:52 +00:00
commit 1d6131bf82
20 changed files with 515 additions and 408 deletions

196
package-lock.json generated
View File

@ -20,8 +20,8 @@
"@mui/lab": "5.0.0-alpha.166", "@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11", "@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.10.0", "@nostr-dev-kit/ndk": "2.11.0",
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1", "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4", "axios": "^1.7.4",
@ -1697,15 +1697,13 @@
} }
}, },
"node_modules/@noble/secp256k1": { "node_modules/@noble/secp256k1": {
"version": "2.0.0", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz",
"integrity": "sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==", "integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==",
"funding": [ "license": "MIT",
{ "funding": {
"type": "individual", "url": "https://paulmillr.com/funding/"
"url": "https://paulmillr.com/funding/" }
}
]
}, },
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
@ -1743,19 +1741,19 @@
} }
}, },
"node_modules/@nostr-dev-kit/ndk": { "node_modules/@nostr-dev-kit/ndk": {
"version": "2.10.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz", "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.11.0.tgz",
"integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==", "integrity": "sha512-FKIMtcVsVcquzrC+yir9lOXHCIHmQ3IKEVCMohqEB7N96HjP2qrI9s5utbjI3lkavFNF5tXg1Gp9ODEo7XCfLA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@noble/curves": "^1.4.0", "@noble/curves": "^1.6.0",
"@noble/hashes": "^1.3.1", "@noble/hashes": "^1.5.0",
"@noble/secp256k1": "^2.0.0", "@noble/secp256k1": "^2.1.0",
"@scure/base": "^1.1.1", "@scure/base": "^1.1.9",
"debug": "^4.3.4", "debug": "^4.3.6",
"light-bolt11-decoder": "^3.0.0", "light-bolt11-decoder": "^3.2.0",
"node-fetch": "^3.3.1",
"nostr-tools": "^2.7.1", "nostr-tools": "^2.7.1",
"tseep": "^1.1.1", "tseep": "^1.2.2",
"typescript-lru-cache": "^2.0.0", "typescript-lru-cache": "^2.0.0",
"utf8-buffer": "^1.0.0", "utf8-buffer": "^1.0.0",
"websocket-polyfill": "^0.0.3" "websocket-polyfill": "^0.0.3"
@ -1765,17 +1763,41 @@
} }
}, },
"node_modules/@nostr-dev-kit/ndk-cache-dexie": { "node_modules/@nostr-dev-kit/ndk-cache-dexie": {
"version": "2.5.1", "version": "2.5.9",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.9.tgz",
"integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==", "integrity": "sha512-SZ5FjON0QPekiC7oW9Hy3JQxG0Oxxtud9LBa1q/A49JV/Qppv1x37nFHxi0XLxEbDgFTNYbaN27Zjfp2NPem2g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@nostr-dev-kit/ndk": "2.10.0", "@nostr-dev-kit/ndk": "2.11.0",
"debug": "^4.3.4", "debug": "^4.3.7",
"dexie": "^4.0.2", "dexie": "^4.0.8",
"nostr-tools": "^2.4.0", "nostr-tools": "^2.4.0",
"typescript-lru-cache": "^2.0.0" "typescript-lru-cache": "^2.0.0"
} }
}, },
"node_modules/@nostr-dev-kit/ndk-cache-dexie/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@nostr-dev-kit/ndk-cache-dexie/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": { "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
@ -1801,6 +1823,15 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@nostr-dev-kit/ndk/node_modules/@scure/base": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz",
"integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": { "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": {
"version": "2.10.4", "version": "2.10.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
@ -1858,6 +1889,24 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/@nostr-dev-kit/ndk/node_modules/tseep": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tseep/-/tseep-1.3.1.tgz",
"integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
@ -6140,14 +6189,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"engines": {
"node": ">= 12"
}
},
"node_modules/dateformat": { "node_modules/dateformat": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
@ -7369,28 +7410,6 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/figures": { "node_modules/figures": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@ -7569,17 +7588,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/from2": { "node_modules/from2": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
@ -9179,9 +9187,10 @@
} }
}, },
"node_modules/light-bolt11-decoder": { "node_modules/light-bolt11-decoder": {
"version": "3.0.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz", "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
"integrity": "sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ==", "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@scure/base": "1.1.1" "@scure/base": "1.1.1"
} }
@ -10256,24 +10265,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
}, },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-emoji": { "node_modules/node-emoji": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz",
@ -10305,23 +10296,6 @@
"url": "https://github.com/sindresorhus/is?sponsor=1" "url": "https://github.com/sindresorhus/is?sponsor=1"
} }
}, },
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
"version": "4.8.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
@ -17451,14 +17425,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@ -31,8 +31,8 @@
"@mui/lab": "5.0.0-alpha.166", "@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11", "@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@nostr-dev-kit/ndk": "2.10.0", "@nostr-dev-kit/ndk": "2.11.0",
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1", "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4", "axios": "^1.7.4",

View File

@ -4,8 +4,6 @@ import { Navigate, Route, Routes } from 'react-router-dom'
import { useAppSelector, useAuth } from './hooks' import { useAppSelector, useAuth } from './hooks'
import { MainLayout } from './layouts/Main' import { MainLayout } from './layouts/Main'
import { appPrivateRoutes, appPublicRoutes } from './routes'
import { import {
privateRoutes, privateRoutes,
publicRoutes, publicRoutes,
@ -16,7 +14,7 @@ import './App.scss'
const App = () => { const App = () => {
const { checkSession } = useAuth() const { checkSession } = useAuth()
const authState = useAppSelector((state) => state.auth) const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
useEffect(() => { useEffect(() => {
if (window.location.hostname === '0.0.0.0') { if (window.location.hostname === '0.0.0.0') {
@ -29,19 +27,9 @@ const App = () => {
checkSession() checkSession()
}, [checkSession]) }, [checkSession])
const handleRootRedirect = () => {
if (authState.loggedIn) return appPrivateRoutes.homePage
const callbackPathEncoded = btoa(
window.location.href.split(`${window.location.origin}/#`)[1]
)
return `${appPublicRoutes.landingPage}?callbackPath=${callbackPathEncoded}`
}
// Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true // Hide route only if loggedIn and r.hiddenWhenLoggedIn are both true
const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => { const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => {
return !authState.loggedIn || !r.hiddenWhenLoggedIn return !isLoggedIn || !r.hiddenWhenLoggedIn
}) })
const privateRouteList = recursiveRouteRenderer(privateRoutes) const privateRouteList = recursiveRouteRenderer(privateRoutes)
@ -49,9 +37,9 @@ const App = () => {
return ( return (
<Routes> <Routes>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
{authState?.loggedIn && privateRouteList}
{publicRoutesList} {publicRoutesList}
<Route path="*" element={<Navigate to={handleRootRedirect()} />} /> {privateRouteList}
<Route path="*" element={<Navigate to={'/'} />} />
</Route> </Route>
</Routes> </Routes>
) )

View File

@ -112,32 +112,37 @@ export const DisplaySigit = ({
</> </>
)} )}
</div> </div>
<div className={styles.itemActions}> {
<Tooltip title="Duplicate" arrow placement="top" disableInteractive> // TODO: enable buttons once feature is ready
<Button false && (
sx={{ <div className={styles.itemActions}>
color: 'var(--primary-main)', <Tooltip title="Duplicate" arrow placement="top" disableInteractive>
minWidth: '34px', <Button
padding: '10px' sx={{
}} color: 'var(--primary-main)',
variant={'text'} minWidth: '34px',
> padding: '10px'
<FontAwesomeIcon icon={faCopy} /> }}
</Button> variant={'text'}
</Tooltip> >
<Tooltip title="Archive" arrow placement="top" disableInteractive> <FontAwesomeIcon icon={faCopy} />
<Button </Button>
sx={{ </Tooltip>
color: 'var(--primary-main)', <Tooltip title="Archive" arrow placement="top" disableInteractive>
minWidth: '34px', <Button
padding: '10px' sx={{
}} color: 'var(--primary-main)',
variant={'text'} minWidth: '34px',
> padding: '10px'
<FontAwesomeIcon icon={faArchive} /> }}
</Button> variant={'text'}
</Tooltip> >
</div> <FontAwesomeIcon icon={faArchive} />
</Button>
</Tooltip>
</div>
)
}
</div> </div>
) )
} }

View File

@ -56,8 +56,7 @@ export const useAuth = () => {
* method will be chosen (extension or keys) * method will be chosen (extension or keys)
* *
* @param pubkey of the user trying to login * @param pubkey of the user trying to login
* @returns url to redirect if authentication successfull * @returns url to redirect if user has no relays set
* or error if otherwise
*/ */
const authAndGetMetadataAndRelaysMap = useCallback( const authAndGetMetadataAndRelaysMap = useCallback(
async (pubkey: string) => { async (pubkey: string) => {
@ -108,7 +107,7 @@ export const useAuth = () => {
dispatch(setRelayMapAction(relayMap)) dispatch(setRelayMapAction(relayMap))
} }
return appPrivateRoutes.homePage return
}, },
[ [
dispatch, dispatch,

View File

@ -12,7 +12,9 @@ import {
import _ from 'lodash' import _ from 'lodash'
import { import {
Event, Event,
finalizeEvent,
generateSecretKey, generateSecretKey,
getEventHash,
getPublicKey, getPublicKey,
kinds, kinds,
UnsignedEvent UnsignedEvent
@ -40,17 +42,21 @@ import {
getDTagForUserAppData, getDTagForUserAppData,
getUserAppDataFromBlossom, getUserAppDataFromBlossom,
hexToNpub, hexToNpub,
nip44Encrypt,
parseJson, parseJson,
randomTimeUpTo2DaysInThePast,
SIGIT_RELAY, SIGIT_RELAY,
unixNow, unixNow,
uploadUserAppDataToBlossom uploadUserAppDataToBlossom
} from '../utils' } from '../utils'
import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError'
export const useNDK = () => { export const useNDK = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { const {
ndk, ndk,
fetchEvent, fetchEvent,
fetchEventFromUserRelays,
fetchEventsFromUserRelays, fetchEventsFromUserRelays,
publish, publish,
getNDKRelayList getNDKRelayList
@ -503,10 +509,139 @@ export const useNDK = () => {
[ndk, usersPubkey, getNDKRelayList] [ndk, usersPubkey, getNDKRelayList]
) )
/**
* Modified {@link UnsignedEvent Unsigned Event} that includes an id
*
* Fields id and created_at are required.
* @see {@link UnsignedEvent}
* @see {@link https://github.com/nostr-protocol/nips/blob/master/17.md#direct-message-kind}
*/
type UnsignedEventWithId = UnsignedEvent & {
id?: string
}
const sendPrivateDirectMessage = useCallback(
async (message: string, receiver: string, subject?: string) => {
if (!receiver) throw new SendDMError(SendDMErrorType.MISSING_RECIEVER)
// Get the direct message preferred relays list
// https://github.com/nostr-protocol/nips/blob/master/17.md#publishing
const preferredRelaysListEvent = await fetchEventFromUserRelays(
{
kinds: [NDKKind.DirectMessageReceiveRelayList],
authors: [receiver]
},
receiver,
UserRelaysType.Read
)
const isRelayTag = (tag: string[]): boolean => tag[0] === 'relay'
const finalRelaysList: string[] = []
if (preferredRelaysListEvent) {
const preferredRelaysList = preferredRelaysListEvent.tags
.filter((t) => isRelayTag(t))
.map((t) => t[1])
finalRelaysList.push(...preferredRelaysList)
}
if (!finalRelaysList.length) {
// Get receiver's read relay list
const ndkRelayList = await getNDKRelayList(receiver).catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
err
)
return null
})
if (ndkRelayList?.readRelayUrls) {
finalRelaysList.push(...ndkRelayList.readRelayUrls)
}
}
if (!finalRelaysList.includes(SIGIT_RELAY)) {
finalRelaysList.push(SIGIT_RELAY)
}
// Generate "sender"
const senderSecret = generateSecretKey()
const senderPubkey = getPublicKey(senderSecret)
// Prepare tags for the message
const tags: string[][] = [['p', receiver]]
// Conversation title
if (subject) tags.push(['subject', subject])
// Create private DM event containing the message and relevant metadata
// TODO: kinds.PrivateDirectMessage (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const dm: UnsignedEventWithId = {
pubkey: senderPubkey,
created_at: unixNow(),
kind: 14,
tags,
content: message
}
// Calculate the hash based on the UnverifiedEvent
dm.id = getEventHash(dm)
// Encrypt the private dm using the sender secret and the receiver's public key
const encryptedDm = nip44Encrypt(dm, senderSecret, receiver)
if (!encryptedDm) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
message,
kind: dm.kind
}
})
}
// Seal the message
// TODO: kinds.Seal (unavailabe in nostr-tools 10/10/2024 at v2.7.0)
const sealedMessage: UnsignedEvent = {
kind: 13, // seal
pubkey: senderPubkey,
content: encryptedDm,
created_at: randomTimeUpTo2DaysInThePast(),
tags: [] // no tags
}
// Finalize and sign the sealed event
const finalizedSeal = finalizeEvent(sealedMessage, senderSecret)
// Encrypt the seal and gift wrap
const finalizedGiftWrap = createWrap(finalizedSeal, receiver)
const ndkEvent = new NDKEvent(ndk, finalizedGiftWrap)
// Publish the finalized gift wrap event (the encrypted DM) to the relays
const publishedOnRelays = await ndkEvent.publish(
NDKRelaySet.fromRelayUrls(finalRelaysList, ndk, true)
)
// Handle cases where publishing to the relays failed
if (publishedOnRelays.size === 0) {
throw new SendDMError(SendDMErrorType.ENCRYPTION_FAILED, {
context: {
receiver,
count: publishedOnRelays.size
}
})
}
// Return true indicating that the DM was successfully sent
return true
},
[fetchEventFromUserRelays, getNDKRelayList, ndk]
)
return { return {
getUsersAppData, getUsersAppData,
subscribeForSigits, subscribeForSigits,
updateUsersAppData, updateUsersAppData,
sendNotification sendNotification,
sendPrivateDirectMessage
} }
} }

View File

@ -1,16 +1,11 @@
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom' import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
import { getPublicKey, nip19 } from 'nostr-tools' import { getPublicKey, nip19 } from 'nostr-tools'
import { init as initNostrLogin } from 'nostr-login' import { init as initNostrLogin } from 'nostr-login'
import { NostrLoginAuthOptions } from 'nostr-login/dist/types' import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
import { AppBar } from '../components/AppBar/AppBar' import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import { import {
useAppDispatch, useAppDispatch,
useAppSelector, useAppSelector,
@ -19,7 +14,6 @@ import {
useNDK, useNDK,
useNDKContext useNDKContext
} from '../hooks' } from '../hooks'
import { import {
restoreState, restoreState,
setUserProfile, setUserProfile,
@ -30,9 +24,7 @@ import {
setUserRobotImage setUserRobotImage
} from '../store/actions' } from '../store/actions'
import { LoginMethod } from '../store/auth/types' import { LoginMethod } from '../store/auth/types'
import { getRoboHashPicture, loadState } from '../utils' import { getRoboHashPicture, loadState } from '../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
export const MainLayout = () => { export const MainLayout = () => {
@ -53,29 +45,32 @@ export const MainLayout = () => {
// Ref to track if `subscribeForSigits` has been called // Ref to track if `subscribeForSigits` has been called
const hasSubscribed = useRef(false) const hasSubscribed = useRef(false)
const navigateAfterLogin = (path: string) => { const navigateAfterLogin = useCallback(
const callbackPath = searchParams.get('callbackPath') (path: string | undefined) => {
const isCallback = window.location.hash.startsWith('#/?callbackPath=')
if (callbackPath) { if (isCallback) {
// base64 decoded path const path = atob(window.location.hash.replace('#/?callbackPath=', ''))
const path = atob(callbackPath) setSearchParams((prev) => {
navigate(path) prev.delete('callbackPath')
return return prev
} })
navigate(path)
navigate(path) return
} }
if (path) navigate(path)
},
[navigate, setSearchParams]
)
const login = useCallback(async () => { const login = useCallback(async () => {
dispatch(updateLoginMethod(LoginMethod.nostrLogin)) try {
dispatch(updateLoginMethod(LoginMethod.nostrLogin))
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const pubkey = await nostrController.capturePublicKey() const pubkey = await nostrController.capturePublicKey()
const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
if (redirectPath) {
navigateAfterLogin(redirectPath) navigateAfterLogin(redirectPath)
} catch (error) {
console.error(`Error occured during login`, error)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch]) }, [dispatch])

View File

@ -45,10 +45,12 @@ import {
uploadToFileStorage, uploadToFileStorage,
DEFAULT_TOOLBOX, DEFAULT_TOOLBOX,
settleAllFullfilfedPromises, settleAllFullfilfedPromises,
parseNostrEvent,
uploadMetaToFileStorage, uploadMetaToFileStorage,
clearSigitDraft, clearSigitDraft,
saveSigitDraft, saveSigitDraft,
getSigitDraft getSigitDraft,
timeout
} from '../../utils' } from '../../utils'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import fileListStyles from '../../components/FileList/style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss'
@ -75,12 +77,14 @@ import { getSigitFile, SigitFile } from '../../utils/file.ts'
import { generateTimestamp } from '../../utils/opentimestamps.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts'
import { Autocomplete } from '@mui/material' import { Autocomplete } from '@mui/material'
import _, { truncate } from 'lodash' import _, { truncate } from 'lodash'
import { SendDMError } from '../../types/errors/SendDMError.ts'
import * as React from 'react' import * as React from 'react'
import { AvatarIconButton } from '../../components/UserAvatarIconButton' import { AvatarIconButton } from '../../components/UserAvatarIconButton'
import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk' import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
import { useNDKContext } from '../../hooks/useNDKContext.ts' import { useNDKContext } from '../../hooks/useNDKContext.ts'
import { useNDK } from '../../hooks/useNDK.ts' import { useNDK } from '../../hooks/useNDK.ts'
import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx' import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
import { TimeoutError } from '../../types/errors/TimeoutError.ts'
type FoundUser = NostrEvent & { npub: string } type FoundUser = NostrEvent & { npub: string }
@ -88,7 +92,8 @@ export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { findMetadata, fetchEventsFromUserRelays } = useNDKContext() const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
const { updateUsersAppData, sendNotification } = useNDK() const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
useNDK()
const { uploadedFiles } = location.state || {} const { uploadedFiles } = location.state || {}
const [currentFile, setCurrentFile] = useState<File>() const [currentFile, setCurrentFile] = useState<File>()
@ -166,8 +171,8 @@ export const CreatePage = () => {
return pubkey return pubkey
} }
const handleSearchUsers = async (searchValue?: string) => { const handleSearchUsers = async () => {
const searchString = searchValue || userSearchInput || undefined const searchString = userSearchInput || undefined
if (!searchString) return if (!searchString) return
@ -175,14 +180,17 @@ export const CreatePage = () => {
const searchTerm = searchString.trim() const searchTerm = searchString.trim()
fetchEventsFromUserRelays( Promise.race([
{ fetchEventsFromUserRelays(
kinds: [0], {
search: searchTerm kinds: [0],
}, search: searchTerm
usersPubkey, },
UserRelaysType.Write usersPubkey,
) UserRelaysType.Write
),
timeout(30000)
])
.then((events) => { .then((events) => {
const nostrEvents = events.map((event) => event.rawEvent()) const nostrEvents = events.map((event) => event.rawEvent())
@ -220,6 +228,9 @@ export const CreatePage = () => {
toast.info('No user found with the provided search term') toast.info('No user found with the provided search term')
}) })
.catch((error) => { .catch((error) => {
if (error instanceof TimeoutError) {
toast.error('Search timed out. Please try again.')
}
console.error(error) console.error(error)
}) })
.finally(() => { .finally(() => {
@ -249,22 +260,23 @@ export const CreatePage = () => {
// If pasted user npub of nip05 is present, we just add the user to the counterparts list // If pasted user npub of nip05 is present, we just add the user to the counterparts list
if (pastedUserNpubOrNip05) { if (pastedUserNpubOrNip05) {
setUserInput(pastedUserNpubOrNip05) setUserInput(pastedUserNpubOrNip05.trim())
setPastedUserNpubOrNip05(undefined) setPastedUserNpubOrNip05(undefined)
} else { } else {
// Otherwize if search already provided some results, user must manually click the search button // Otherwise if search already provided some results, user must manually click the search button
if (!foundUsers.length) { if (!foundUsers.length) {
const searchTerm = userSearchInput.trim()
// If it's NIP05 (includes @ or is a valid domain) send request to .well-known // If it's NIP05 (includes @ or is a valid domain) send request to .well-known
const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/ const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/
if (domainRegex.test(userSearchInput)) { if (searchTerm.startsWith('_@') || domainRegex.test(searchTerm)) {
setSearchUsersLoading(true) setSearchUsersLoading(true)
const pubkey = await handleSearchUserNip05(userSearchInput) const pubkey = await handleSearchUserNip05(searchTerm)
setSearchUsersLoading(false) setSearchUsersLoading(false)
if (pubkey) { if (pubkey) {
setUserInput(userSearchInput) setUserInput(searchTerm)
} else { } else {
toast.error(`No user found with the NIP05: ${userSearchInput}`) toast.error(`No user found with the NIP05: ${userSearchInput}`)
} }
@ -462,7 +474,7 @@ export const CreatePage = () => {
setUserSearchInput('') setUserSearchInput('')
if (input.startsWith('npub')) { if (input.startsWith('npub1')) {
return handleAddNpubUser(input) return handleAddNpubUser(input)
} }
@ -977,7 +989,29 @@ export const CreatePage = () => {
toast.error('Failed to publish notifications') toast.error('Failed to publish notifications')
}) })
const isFirstSigner = signers[0].pubkey === usersPubkey const isFirstSigner =
signers.length > 0 && signers[0].pubkey === usersPubkey
// Don't send notification if creator is next signer
if (signers.length > 0 && !isFirstSigner) {
// Send DM to the next signer
setLoadingSpinnerDesc('Sending DMs')
const nextSigner = signers[0].pubkey
const createSignatureEvent = parseNostrEvent(meta.createSignature)
const { id } = createSignatureEvent
try {
await sendPrivateDirectMessage(
`Sigit created, visit ${window.location.origin}/#/sign/${id}`,
npubToHex(nextSigner)!
)
} catch (error) {
if (error instanceof SendDMError) {
toast.error(error.message)
}
console.error(error)
}
}
if (isFirstSigner) { if (isFirstSigner) {
navigate(appPrivateRoutes.sign, { state: { meta } }) navigate(appPrivateRoutes.sign, { state: { meta } })
} else { } else {
@ -1087,17 +1121,13 @@ export const CreatePage = () => {
} }
// Seems like it's npub format // Seems like it's npub format
if (value.startsWith('npub')) { if (value.trim().startsWith('npub1')) {
// We will try to convert npub to hex and if it's successfull that means setPastedUserNpubOrNip05(value.trim())
// npub is valid } else if (value.trim().startsWith('nsec1')) {
const validHexPubkey = npubToHex(value) toast.warn('Oops - never paste your nsec into a website! Key deleted.')
if (searchFieldRef.current) searchFieldRef.current.value = ''
if (validHexPubkey) { setUserSearchInput('')
// Arm the manual user npub add after enter is hit, we don't want to trigger search return
setPastedUserNpubOrNip05(value)
} else {
disarmAddOnEnter()
}
} else { } else {
// Disarm the add user on enter hit, and trigger search after 1 second // Disarm the add user on enter hit, and trigger search after 1 second
disarmAddOnEnter() disarmAddOnEnter()
@ -1257,7 +1287,7 @@ export const CreatePage = () => {
{!pastedUserNpubOrNip05 ? ( {!pastedUserNpubOrNip05 ? (
<Button <Button
disabled={!userSearchInput || searchUsersLoading} disabled={!userSearchInput || searchUsersLoading}
onClick={() => handleSearchUsers()} onClick={handleSearchUsers}
variant="contained" variant="contained"
aria-label="Add" aria-label="Add"
className={styles.counterpartToggleButton} className={styles.counterpartToggleButton}
@ -1271,7 +1301,7 @@ export const CreatePage = () => {
) : ( ) : (
<Button <Button
onClick={() => { onClick={() => {
setUserInput(userSearchInput) setUserInput(userSearchInput.trim())
}} }}
variant="contained" variant="contained"
aria-label="Add" aria-label="Add"

View File

@ -1,7 +1,5 @@
import { Box, Button } from '@mui/material' import { Box, Button } from '@mui/material'
import { useEffect } from 'react' import { Outlet } from 'react-router-dom'
import { Outlet, useLocation } from 'react-router-dom'
import { saveVisitedLink } from '../../utils'
import { CardComponent } from '../../components/Landing/CardComponent/CardComponent' import { CardComponent } from '../../components/Landing/CardComponent/CardComponent'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import styles from './style.module.scss' import styles from './style.module.scss'
@ -20,13 +18,19 @@ import {
import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack' import { FontAwesomeIconStack } from '../../components/FontAwesomeIconStack'
import { Footer } from '../../components/Footer/Footer' import { Footer } from '../../components/Footer/Footer'
import { launch as launchNostrLoginDialog } from 'nostr-login' import { launch as launchNostrLoginDialog } from 'nostr-login'
import { useDidMount } from '../../hooks'
export const LandingPage = () => { export const LandingPage = () => {
const location = useLocation()
const onSignInClick = async () => { const onSignInClick = async () => {
launchNostrLoginDialog() launchNostrLoginDialog()
} }
useDidMount(() => {
const isCallback = window.location.hash.startsWith('#/?callbackPath=')
// Open nostr login if detect callback
if (isCallback) {
onSignInClick()
}
})
const cards = [ const cards = [
{ {
@ -101,10 +105,6 @@ export const LandingPage = () => {
} }
] ]
useEffect(() => {
saveVisitedLink(location.pathname, location.search)
}, [location])
return ( return (
<div className={styles.background}> <div className={styles.background}>
<div <div

View File

@ -1,4 +1,3 @@
import ClearIcon from '@mui/icons-material/Clear'
import InputIcon from '@mui/icons-material/Input' import InputIcon from '@mui/icons-material/Input'
import IosShareIcon from '@mui/icons-material/IosShare' import IosShareIcon from '@mui/icons-material/IosShare'
import { import {
@ -9,36 +8,12 @@ import {
ListSubheader, ListSubheader,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
import { useState } from 'react'
import { toast } from 'react-toastify'
import { localCache } from '../../../services'
import { LoadingSpinner } from '../../../components/LoadingSpinner'
import { Container } from '../../../components/Container' import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer' import { Footer } from '../../../components/Footer/Footer'
export const CacheSettingsPage = () => { export const CacheSettingsPage = () => {
const theme = useTheme() const theme = useTheme()
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const handleClearData = async () => {
setIsLoading(true)
setLoadingSpinnerDesc('Clearing cache data')
localCache
.clearCacheData()
.then(() => {
toast.success('cleared cached data')
})
.catch((err) => {
console.log('An error occurred in clearing cache data', err)
toast.error(err.message || 'An error occurred in clearing cache data')
})
.finally(() => {
setIsLoading(false)
})
}
const listItem = (label: string) => { const listItem = (label: string) => {
return ( return (
<ListItemText <ListItemText
@ -53,7 +28,6 @@ export const CacheSettingsPage = () => {
return ( return (
<> <>
<Container> <Container>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<List <List
sx={{ sx={{
width: '100%', width: '100%',
@ -87,13 +61,6 @@ export const CacheSettingsPage = () => {
</ListItemIcon> </ListItemIcon>
{listItem('Import (coming soon)')} {listItem('Import (coming soon)')}
</ListItemButton> </ListItemButton>
<ListItemButton onClick={handleClearData}>
<ListItemIcon>
<ClearIcon sx={{ color: theme.palette.error.main }} />
</ListItemIcon>
{listItem('Clear Cache')}
</ListItemButton>
</List> </List>
</Container> </Container>
<Footer /> <Footer />

View File

@ -28,7 +28,8 @@ import {
signEventForMetaFile, signEventForMetaFile,
unixNow, unixNow,
updateMarks, updateMarks,
uploadMetaToFileStorage uploadMetaToFileStorage,
parseNostrEvent
} from '../../utils' } from '../../utils'
import { CurrentUserMark, Mark } from '../../types/mark.ts' import { CurrentUserMark, Mark } from '../../types/mark.ts'
import PdfMarking from '../../components/PDFView/PdfMarking.tsx' import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
@ -36,12 +37,14 @@ import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
import { generateTimestamp } from '../../utils/opentimestamps.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts'
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
import { useNDK } from '../../hooks/useNDK.ts' import { useNDK } from '../../hooks/useNDK.ts'
import { SendDMError } from '../../types/errors/SendDMError.ts'
export const SignPage = () => { export const SignPage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const params = useParams() const params = useParams()
const { updateUsersAppData, sendNotification } = useNDK() const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
useNDK()
const usersAppData = useAppSelector((state) => state.userAppData) const usersAppData = useAppSelector((state) => state.userAppData)
@ -602,6 +605,66 @@ export const SignPage = () => {
toast.error('Failed to publish notifications') toast.error('Failed to publish notifications')
}) })
// Send DMs
setLoadingSpinnerDesc('Sending DMs')
const createSignatureEvent = parseNostrEvent(meta.createSignature)
const { id } = createSignatureEvent
if (isLastSigner) {
// Final sign sends to everyone (creator, signers, viewers - /verify)
const areSent: boolean[] = Array(users.length).fill(false)
for (let i = 0; i < users.length; i++) {
try {
areSent[i] = await sendPrivateDirectMessage(
`Sigit completed, visit ${window.location.origin}/#/verify/${id}`,
npubToHex(users[i])!
)
} catch (error) {
if (error instanceof SendDMError) {
toast.error(error.message)
}
console.error(error)
}
}
if (areSent.some((r) => r)) {
toast.success(
`DMs sent ${areSent.filter((r) => r).length}/${users.length}`
)
}
} else {
// Notify the creator and
// the next signer (/sign).
try {
await sendPrivateDirectMessage(
`Sigit signed by ${usersNpub}, visit ${window.location.origin}/#/sign/${id}`,
npubToHex(submittedBy!)!
)
} catch (error) {
if (error instanceof SendDMError) {
toast.error(error.message)
}
console.error(error)
}
// No need to notify creator twice, skipping
const currentSignerIndex = signers.indexOf(usersNpub)
const nextSigner = npubToHex(signers[currentSignerIndex + 1])
if (nextSigner !== submittedBy) {
try {
await sendPrivateDirectMessage(
`You're the next signer, visit ${window.location.origin}/#/sign/${id}`,
nextSigner!
)
} catch (error) {
if (error instanceof SendDMError) {
toast.error(error.message)
}
console.error(error)
}
}
}
setIsLoading(false) setIsLoading(false)
} }

View File

@ -0,0 +1,21 @@
import { Navigate, useLocation } from 'react-router-dom'
import { useAppSelector } from '../hooks'
import { appPublicRoutes } from '.'
export function PrivateRoute({ children }: { children: JSX.Element }) {
const location = useLocation()
const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
if (!isLoggedIn) {
return (
<Navigate
to={{
pathname: appPublicRoutes.landingPage,
search: `?callbackPath=${btoa(location.pathname)}`
}}
replace
/>
)
}
return children
}

View File

@ -11,6 +11,7 @@ import { RelaysPage } from '../pages/settings/relays'
import { SettingsPage } from '../pages/settings/Settings' import { SettingsPage } from '../pages/settings/Settings'
import { SignPage } from '../pages/sign' import { SignPage } from '../pages/sign'
import { VerifyPage } from '../pages/verify' import { VerifyPage } from '../pages/verify'
import { PrivateRoute } from './PrivateRoute'
/** /**
* Helper type allows for extending react-router-dom's **RouteProps** with generic type * Helper type allows for extending react-router-dom's **RouteProps** with generic type
@ -70,34 +71,66 @@ export const publicRoutes: PublicRouteProps[] = [
export const privateRoutes = [ export const privateRoutes = [
{ {
path: appPrivateRoutes.homePage, path: appPrivateRoutes.homePage,
element: <HomePage /> element: (
<PrivateRoute>
<HomePage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.create, path: appPrivateRoutes.create,
element: <CreatePage /> element: (
<PrivateRoute>
<CreatePage />
</PrivateRoute>
)
}, },
{ {
path: `${appPrivateRoutes.sign}/:id?`, path: `${appPrivateRoutes.sign}/:id?`,
element: <SignPage /> element: (
<PrivateRoute>
<SignPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.settings, path: appPrivateRoutes.settings,
element: <SettingsPage /> element: (
<PrivateRoute>
<SettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.profileSettings, path: appPrivateRoutes.profileSettings,
element: <ProfileSettingsPage /> element: (
<PrivateRoute>
<ProfileSettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.cacheSettings, path: appPrivateRoutes.cacheSettings,
element: <CacheSettingsPage /> element: (
<PrivateRoute>
<CacheSettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.relays, path: appPrivateRoutes.relays,
element: <RelaysPage /> element: (
<PrivateRoute>
<RelaysPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.nostrLogin, path: appPrivateRoutes.nostrLogin,
element: <NostrLoginPage /> element: (
<PrivateRoute>
<NostrLoginPage />
</PrivateRoute>
)
} }
] ]

View File

@ -1,86 +0,0 @@
import { IDBPDatabase, openDB } from 'idb'
import { Event } from 'nostr-tools'
import { CachedEvent } from '../../types'
import { SchemaV2 } from './schema'
class LocalCache {
// Static property to hold the single instance of LocalCache
private static instance: LocalCache | null = null
private db!: IDBPDatabase<SchemaV2>
// Private constructor to prevent direct instantiation
private constructor() {}
// Method to initialize the database
private async init() {
this.db = await openDB<SchemaV2>('sigit-cache', 2, {
upgrade(db, oldVersion) {
if (oldVersion < 1) {
db.createObjectStore('userMetadata', { keyPath: 'event.pubkey' })
}
if (oldVersion < 2) {
const v6 = db as unknown as IDBPDatabase<SchemaV2>
v6.createObjectStore('userRelayListMetadata', {
keyPath: 'event.pubkey'
})
}
}
})
}
// Static method to get the single instance of LocalCache
public static async getInstance(): Promise<LocalCache> {
// If the instance doesn't exist, create it
if (!LocalCache.instance) {
LocalCache.instance = new LocalCache()
await LocalCache.instance.init()
}
// Return the single instance of LocalCache
return LocalCache.instance
}
// Method to add user metadata
public async addUserMetadata(event: Event) {
await this.db.put('userMetadata', { event, cachedAt: Date.now() })
}
// Method to get user metadata by key
public async getUserMetadata(key: string): Promise<CachedEvent | null> {
const data = await this.db.get('userMetadata', key)
return data || null
}
// Method to delete user metadata by key
public async deleteUserMetadata(key: string) {
await this.db.delete('userMetadata', key)
}
public async addUserRelayListMetadata(event: Event) {
await this.db.put('userRelayListMetadata', { event, cachedAt: Date.now() })
}
public async getUserRelayListMetadata(
key: string
): Promise<CachedEvent | null> {
const data = await this.db.get('userRelayListMetadata', key)
return data || null
}
public async deleteUserRelayListMetadata(key: string) {
await this.db.delete('userRelayListMetadata', key)
}
// Method to clear cache data
public async clearCacheData() {
// Clear the 'userMetadata' store in the IndexedDB database
await this.db.clear('userMetadata')
// Reload the current page to ensure any cached data is reset
window.location.reload()
}
}
// Export the single instance of LocalCache
export const localCache = await LocalCache.getInstance()

View File

@ -1,16 +0,0 @@
import { DBSchema } from 'idb'
import { CachedEvent } from '../../types'
export interface SchemaV1 extends DBSchema {
userMetadata: {
key: string
value: CachedEvent
}
}
export interface SchemaV2 extends SchemaV1 {
userRelayListMetadata: {
key: string
value: CachedEvent
}
}

View File

@ -1,2 +1 @@
export * from './cache'
export * from './signer' export * from './signer'

View File

@ -0,0 +1,23 @@
import { Jsonable } from '.'
export enum SendDMErrorType {
'MISSING_RECIEVER' = 'Sending DM failed. Reciever is required.',
'ENCRYPTION_FAILED' = 'Sending DM failed. An error occurred in encrypting dm message.',
'RELAY_PUBLISH_FAILED' = 'Sending DM failed. Publishing events failed.'
}
export class SendDMError extends Error {
public readonly context?: Jsonable
constructor(
message: string,
options: { cause?: Error; context?: Jsonable } = {}
) {
const { cause, context } = options
super(message, { cause })
this.name = this.constructor.name
this.context = context
}
}

View File

@ -26,30 +26,6 @@ export const clearState = () => {
localStorage.removeItem('state') localStorage.removeItem('state')
} }
export const saveVisitedLink = (pathname: string, search: string) => {
localStorage.setItem(
'visitedLink',
JSON.stringify({
pathname,
search
})
)
}
export const getVisitedLink = () => {
const visitedLink = localStorage.getItem('visitedLink')
if (!visitedLink) return null
try {
return JSON.parse(visitedLink) as {
pathname: string
search: string
}
} catch {
return null
}
}
export const saveAuthToken = (token: string) => { export const saveAuthToken = (token: string) => {
localStorage.setItem('authToken', token) localStorage.setItem('authToken', token)
} }

View File

@ -6,6 +6,7 @@ import {
Event, Event,
EventTemplate, EventTemplate,
UnsignedEvent, UnsignedEvent,
VerifiedEvent,
finalizeEvent, finalizeEvent,
generateSecretKey, generateSecretKey,
getEventHash, getEventHash,
@ -214,6 +215,12 @@ export const toUnixTimestamp = (date: number | Date) => {
export const fromUnixTimestamp = (unix: number) => { export const fromUnixTimestamp = (unix: number) => {
return unix * 1000 return unix * 1000
} }
export const randomTimeUpTo2DaysInThePast = (): number => {
const now = Date.now()
const twoDaysInMilliseconds = 2 * 24 * 60 * 60 * 1000
const randomPastTime = now - Math.floor(Math.random() * twoDaysInMilliseconds)
return toUnixTimestamp(randomPastTime)
}
/** /**
* Generate nip44 conversation key * Generate nip44 conversation key
@ -263,19 +270,21 @@ export const countLeadingZeroes = (hex: string) => {
/** /**
* Function to create a wrapped event with PoW * Function to create a wrapped event with PoW
* @param event Original event to be wrapped * @param event Original event to be wrapped (can be unsigned or verified)
* @param receiver Public key of the receiver * @param receiver Public key of the receiver
* @param difficulty PoW difficulty level (default is 20)
* @returns * @returns
*/ */
// //
export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => { export const createWrap = (
event: UnsignedEvent | VerifiedEvent,
receiver: string
) => {
// Generate a random secret key and its corresponding public key // Generate a random secret key and its corresponding public key
const randomKey = generateSecretKey() const randomKey = generateSecretKey()
const pubkey = getPublicKey(randomKey) const pubkey = getPublicKey(randomKey)
// Encrypt the event content using nip44 encryption // Encrypt the event content using nip44 encryption
const content = nip44Encrypt(unsignedEvent, randomKey, receiver) const content = nip44Encrypt(event, randomKey, receiver)
// Initialize nonce and leadingZeroes for PoW calculation // Initialize nonce and leadingZeroes for PoW calculation
let nonce = 0 let nonce = 0
@ -286,11 +295,12 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
// Create an unsigned event with the necessary fields // Create an unsigned event with the necessary fields
// TODO: kinds.GiftWrap (wrong kind number in nostr-tools 10/11/2024 at v2.7.2)
const event: UnsignedEvent = { const event: UnsignedEvent = {
kind: 1059, // Event kind kind: 1059, // Event kind
content, // Encrypted content content, // Encrypted content
pubkey, // Public key of the creator pubkey, // Public key of the creator
created_at: unixNow(), // Current timestamp created_at: randomTimeUpTo2DaysInThePast(),
tags: [ tags: [
// Tags including receiver and nonce // Tags including receiver and nonce
['p', receiver], ['p', receiver],

View File

@ -37,7 +37,6 @@ export const getRelayMapFromNDKRelayList = (ndkRelayList: NDKRelayList) => {
export const getDefaultRelayMap = (): RelayMap => ({ export const getDefaultRelayMap = (): RelayMap => ({
[SIGIT_RELAY]: { write: true, read: true } [SIGIT_RELAY]: { write: true, read: true }
}) })
/** /**
* Publishes relay map. * Publishes relay map.
* @param relayMap - relay map. * @param relayMap - relay map.