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 ? (