diff --git a/package-lock.json b/package-lock.json
index 9e327f4..b439f36 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,8 +20,8 @@
"@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
- "@nostr-dev-kit/ndk": "2.10.0",
- "@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
+ "@nostr-dev-kit/ndk": "2.11.0",
+ "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
@@ -1697,15 +1697,13 @@
}
},
"node_modules/@noble/secp256k1": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.0.0.tgz",
- "integrity": "sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- ]
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz",
+ "integrity": "sha512-l7r5oEQym9Us7EAigzg30/PQAvynhMt2uoYtT3t26eGDVm9Yii5mZ5jWSWmZ/oSIR2Et0xfc6DXrG0bZ787V3w==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@@ -1743,19 +1741,19 @@
}
},
"node_modules/@nostr-dev-kit/ndk": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz",
- "integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.11.0.tgz",
+ "integrity": "sha512-FKIMtcVsVcquzrC+yir9lOXHCIHmQ3IKEVCMohqEB7N96HjP2qrI9s5utbjI3lkavFNF5tXg1Gp9ODEo7XCfLA==",
+ "license": "MIT",
"dependencies": {
- "@noble/curves": "^1.4.0",
- "@noble/hashes": "^1.3.1",
- "@noble/secp256k1": "^2.0.0",
- "@scure/base": "^1.1.1",
- "debug": "^4.3.4",
- "light-bolt11-decoder": "^3.0.0",
- "node-fetch": "^3.3.1",
+ "@noble/curves": "^1.6.0",
+ "@noble/hashes": "^1.5.0",
+ "@noble/secp256k1": "^2.1.0",
+ "@scure/base": "^1.1.9",
+ "debug": "^4.3.6",
+ "light-bolt11-decoder": "^3.2.0",
"nostr-tools": "^2.7.1",
- "tseep": "^1.1.1",
+ "tseep": "^1.2.2",
"typescript-lru-cache": "^2.0.0",
"utf8-buffer": "^1.0.0",
"websocket-polyfill": "^0.0.3"
@@ -1765,17 +1763,41 @@
}
},
"node_modules/@nostr-dev-kit/ndk-cache-dexie": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz",
- "integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==",
+ "version": "2.5.9",
+ "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.9.tgz",
+ "integrity": "sha512-SZ5FjON0QPekiC7oW9Hy3JQxG0Oxxtud9LBa1q/A49JV/Qppv1x37nFHxi0XLxEbDgFTNYbaN27Zjfp2NPem2g==",
+ "license": "MIT",
"dependencies": {
- "@nostr-dev-kit/ndk": "2.10.0",
- "debug": "^4.3.4",
- "dexie": "^4.0.2",
+ "@nostr-dev-kit/ndk": "2.11.0",
+ "debug": "^4.3.7",
+ "dexie": "^4.0.8",
"nostr-tools": "^2.4.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": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
@@ -1801,6 +1823,15 @@
"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": {
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
@@ -1858,6 +1889,24 @@
"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": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
@@ -6140,14 +6189,6 @@
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
@@ -7369,28 +7410,6 @@
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -7569,17 +7588,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": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
@@ -9179,9 +9187,10 @@
}
},
"node_modules/light-bolt11-decoder": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz",
- "integrity": "sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
+ "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
+ "license": "MIT",
"dependencies": {
"@scure/base": "1.1.1"
}
@@ -10256,24 +10265,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"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": {
"version": "2.2.0",
"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"
}
},
- "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": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
@@ -17451,14 +17425,6 @@
"dev": true,
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/package.json b/package.json
index d650959..7e372f1 100644
--- a/package.json
+++ b/package.json
@@ -31,8 +31,8 @@
"@mui/lab": "5.0.0-alpha.166",
"@mui/material": "5.15.11",
"@noble/hashes": "^1.4.0",
- "@nostr-dev-kit/ndk": "2.10.0",
- "@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
+ "@nostr-dev-kit/ndk": "2.11.0",
+ "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4",
diff --git a/src/App.tsx b/src/App.tsx
index 3829ba6..d10dc0d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,8 +4,6 @@ import { Navigate, Route, Routes } from 'react-router-dom'
import { useAppSelector, useAuth } from './hooks'
import { MainLayout } from './layouts/Main'
-
-import { appPrivateRoutes, appPublicRoutes } from './routes'
import {
privateRoutes,
publicRoutes,
@@ -16,7 +14,7 @@ import './App.scss'
const App = () => {
const { checkSession } = useAuth()
- const authState = useAppSelector((state) => state.auth)
+ const isLoggedIn = useAppSelector((state) => state.auth?.loggedIn)
useEffect(() => {
if (window.location.hostname === '0.0.0.0') {
@@ -29,19 +27,9 @@ const App = () => {
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
const publicRoutesList = recursiveRouteRenderer(publicRoutes, (r) => {
- return !authState.loggedIn || !r.hiddenWhenLoggedIn
+ return !isLoggedIn || !r.hiddenWhenLoggedIn
})
const privateRouteList = recursiveRouteRenderer(privateRoutes)
@@ -49,9 +37,9 @@ const App = () => {
return (
}>
- {authState?.loggedIn && privateRouteList}
{publicRoutesList}
- } />
+ {privateRouteList}
+ } />
)
diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx
index 5147b45..20550b3 100644
--- a/src/components/DisplaySigit/index.tsx
+++ b/src/components/DisplaySigit/index.tsx
@@ -112,32 +112,37 @@ export const DisplaySigit = ({
>
)}
-
-
-
-
-
-
-
-
+ {
+ // TODO: enable buttons once feature is ready
+ false && (
+
+
+
+
+
+
+
+
+ )
+ }
)
}
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
index e0e75fb..7199a51 100644
--- a/src/hooks/useAuth.ts
+++ b/src/hooks/useAuth.ts
@@ -56,8 +56,7 @@ export const useAuth = () => {
* method will be chosen (extension or keys)
*
* @param pubkey of the user trying to login
- * @returns url to redirect if authentication successfull
- * or error if otherwise
+ * @returns url to redirect if user has no relays set
*/
const authAndGetMetadataAndRelaysMap = useCallback(
async (pubkey: string) => {
@@ -108,7 +107,7 @@ export const useAuth = () => {
dispatch(setRelayMapAction(relayMap))
}
- return appPrivateRoutes.homePage
+ return
},
[
dispatch,
diff --git a/src/hooks/useNDK.ts b/src/hooks/useNDK.ts
index ad9e54f..c6ec41d 100644
--- a/src/hooks/useNDK.ts
+++ b/src/hooks/useNDK.ts
@@ -12,7 +12,9 @@ import {
import _ from 'lodash'
import {
Event,
+ finalizeEvent,
generateSecretKey,
+ getEventHash,
getPublicKey,
kinds,
UnsignedEvent
@@ -40,17 +42,21 @@ import {
getDTagForUserAppData,
getUserAppDataFromBlossom,
hexToNpub,
+ nip44Encrypt,
parseJson,
+ randomTimeUpTo2DaysInThePast,
SIGIT_RELAY,
unixNow,
uploadUserAppDataToBlossom
} from '../utils'
+import { SendDMError, SendDMErrorType } from '../types/errors/SendDMError'
export const useNDK = () => {
const dispatch = useAppDispatch()
const {
ndk,
fetchEvent,
+ fetchEventFromUserRelays,
fetchEventsFromUserRelays,
publish,
getNDKRelayList
@@ -503,10 +509,139 @@ export const useNDK = () => {
[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 {
getUsersAppData,
subscribeForSigits,
updateUsersAppData,
- sendNotification
+ sendNotification,
+ sendPrivateDirectMessage
}
}
diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx
index 85daf75..2106931 100644
--- a/src/layouts/Main.tsx
+++ b/src/layouts/Main.tsx
@@ -1,16 +1,11 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'
-
import { getPublicKey, nip19 } from 'nostr-tools'
-
import { init as initNostrLogin } from 'nostr-login'
import { NostrLoginAuthOptions } from 'nostr-login/dist/types'
-
import { AppBar } from '../components/AppBar/AppBar'
import { LoadingSpinner } from '../components/LoadingSpinner'
-
import { NostrController } from '../controllers'
-
import {
useAppDispatch,
useAppSelector,
@@ -19,7 +14,6 @@ import {
useNDK,
useNDKContext
} from '../hooks'
-
import {
restoreState,
setUserProfile,
@@ -30,9 +24,7 @@ import {
setUserRobotImage
} from '../store/actions'
import { LoginMethod } from '../store/auth/types'
-
import { getRoboHashPicture, loadState } from '../utils'
-
import styles from './style.module.scss'
export const MainLayout = () => {
@@ -53,29 +45,32 @@ export const MainLayout = () => {
// Ref to track if `subscribeForSigits` has been called
const hasSubscribed = useRef(false)
- const navigateAfterLogin = (path: string) => {
- const callbackPath = searchParams.get('callbackPath')
-
- if (callbackPath) {
- // base64 decoded path
- const path = atob(callbackPath)
- navigate(path)
- return
- }
-
- navigate(path)
- }
+ const navigateAfterLogin = useCallback(
+ (path: string | undefined) => {
+ const isCallback = window.location.hash.startsWith('#/?callbackPath=')
+ if (isCallback) {
+ const path = atob(window.location.hash.replace('#/?callbackPath=', ''))
+ setSearchParams((prev) => {
+ prev.delete('callbackPath')
+ return prev
+ })
+ navigate(path)
+ return
+ }
+ if (path) navigate(path)
+ },
+ [navigate, setSearchParams]
+ )
const login = useCallback(async () => {
- dispatch(updateLoginMethod(LoginMethod.nostrLogin))
-
- const nostrController = NostrController.getInstance()
- const pubkey = await nostrController.capturePublicKey()
-
- const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
-
- if (redirectPath) {
+ try {
+ dispatch(updateLoginMethod(LoginMethod.nostrLogin))
+ const nostrController = NostrController.getInstance()
+ const pubkey = await nostrController.capturePublicKey()
+ const redirectPath = await authAndGetMetadataAndRelaysMap(pubkey)
navigateAfterLogin(redirectPath)
+ } catch (error) {
+ console.error(`Error occured during login`, error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index 60011b7..2bb0642 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -45,10 +45,12 @@ import {
uploadToFileStorage,
DEFAULT_TOOLBOX,
settleAllFullfilfedPromises,
+ parseNostrEvent,
uploadMetaToFileStorage,
clearSigitDraft,
saveSigitDraft,
- getSigitDraft
+ getSigitDraft,
+ timeout
} from '../../utils'
import { Container } from '../../components/Container'
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 { Autocomplete } from '@mui/material'
import _, { truncate } from 'lodash'
+import { SendDMError } from '../../types/errors/SendDMError.ts'
import * as React from 'react'
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
import { useNDKContext } from '../../hooks/useNDKContext.ts'
import { useNDK } from '../../hooks/useNDK.ts'
import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'
+import { TimeoutError } from '../../types/errors/TimeoutError.ts'
type FoundUser = NostrEvent & { npub: string }
@@ -88,7 +92,8 @@ export const CreatePage = () => {
const navigate = useNavigate()
const location = useLocation()
const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
- const { updateUsersAppData, sendNotification } = useNDK()
+ const { updateUsersAppData, sendNotification, sendPrivateDirectMessage } =
+ useNDK()
const { uploadedFiles } = location.state || {}
const [currentFile, setCurrentFile] = useState()
@@ -166,8 +171,8 @@ export const CreatePage = () => {
return pubkey
}
- const handleSearchUsers = async (searchValue?: string) => {
- const searchString = searchValue || userSearchInput || undefined
+ const handleSearchUsers = async () => {
+ const searchString = userSearchInput || undefined
if (!searchString) return
@@ -175,14 +180,17 @@ export const CreatePage = () => {
const searchTerm = searchString.trim()
- fetchEventsFromUserRelays(
- {
- kinds: [0],
- search: searchTerm
- },
- usersPubkey,
- UserRelaysType.Write
- )
+ Promise.race([
+ fetchEventsFromUserRelays(
+ {
+ kinds: [0],
+ search: searchTerm
+ },
+ usersPubkey,
+ UserRelaysType.Write
+ ),
+ timeout(30000)
+ ])
.then((events) => {
const nostrEvents = events.map((event) => event.rawEvent())
@@ -220,6 +228,9 @@ export const CreatePage = () => {
toast.info('No user found with the provided search term')
})
.catch((error) => {
+ if (error instanceof TimeoutError) {
+ toast.error('Search timed out. Please try again.')
+ }
console.error(error)
})
.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 (pastedUserNpubOrNip05) {
- setUserInput(pastedUserNpubOrNip05)
+ setUserInput(pastedUserNpubOrNip05.trim())
setPastedUserNpubOrNip05(undefined)
} 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) {
+ const searchTerm = userSearchInput.trim()
// 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,}$/
- if (domainRegex.test(userSearchInput)) {
+ if (searchTerm.startsWith('_@') || domainRegex.test(searchTerm)) {
setSearchUsersLoading(true)
- const pubkey = await handleSearchUserNip05(userSearchInput)
+ const pubkey = await handleSearchUserNip05(searchTerm)
setSearchUsersLoading(false)
if (pubkey) {
- setUserInput(userSearchInput)
+ setUserInput(searchTerm)
} else {
toast.error(`No user found with the NIP05: ${userSearchInput}`)
}
@@ -462,7 +474,7 @@ export const CreatePage = () => {
setUserSearchInput('')
- if (input.startsWith('npub')) {
+ if (input.startsWith('npub1')) {
return handleAddNpubUser(input)
}
@@ -977,7 +989,29 @@ export const CreatePage = () => {
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) {
navigate(appPrivateRoutes.sign, { state: { meta } })
} else {
@@ -1087,17 +1121,13 @@ export const CreatePage = () => {
}
// Seems like it's npub format
- if (value.startsWith('npub')) {
- // We will try to convert npub to hex and if it's successfull that means
- // npub is valid
- const validHexPubkey = npubToHex(value)
-
- if (validHexPubkey) {
- // Arm the manual user npub add after enter is hit, we don't want to trigger search
- setPastedUserNpubOrNip05(value)
- } else {
- disarmAddOnEnter()
- }
+ if (value.trim().startsWith('npub1')) {
+ setPastedUserNpubOrNip05(value.trim())
+ } else if (value.trim().startsWith('nsec1')) {
+ toast.warn('Oops - never paste your nsec into a website! Key deleted.')
+ if (searchFieldRef.current) searchFieldRef.current.value = ''
+ setUserSearchInput('')
+ return
} else {
// Disarm the add user on enter hit, and trigger search after 1 second
disarmAddOnEnter()
@@ -1257,7 +1287,7 @@ export const CreatePage = () => {
{!pastedUserNpubOrNip05 ? (