diff --git a/package-lock.json b/package-lock.json index 8fbc648..c000deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "jszip": "3.10.1", "lodash": "4.17.21", "mui-file-input": "4.0.4", - "nostr-tools": "2.3.1", + "nostr-tools": "2.7.0", "react": "^18.2.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", @@ -34,7 +34,8 @@ "react-router-dom": "6.22.1", "react-toastify": "10.0.4", "redux": "5.0.1", - "tseep": "1.2.1" + "tseep": "1.2.1", + "uuid": "10.0.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", @@ -42,6 +43,7 @@ "@types/lodash": "4.14.202", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", + "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", @@ -2138,6 +2140,12 @@ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", @@ -4137,9 +4145,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.3.1.tgz", - "integrity": "sha512-qjKx2C3EzwiQOe2LPSPyCnp07pGz1pWaWjDXcm+L2y2c8iTECbvlzujDANm3nJUjWL5+LVRUVDovTZ1a/DC4Bg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz", + "integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==", "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", @@ -5211,6 +5219,18 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index dc00ccf..658fafb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "jszip": "3.10.1", "lodash": "4.17.21", "mui-file-input": "4.0.4", - "nostr-tools": "2.3.1", + "nostr-tools": "2.7.0", "react": "^18.2.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", @@ -40,7 +40,8 @@ "react-router-dom": "6.22.1", "react-toastify": "10.0.4", "redux": "5.0.1", - "tseep": "1.2.1" + "tseep": "1.2.1", + "uuid": "10.0.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", @@ -48,6 +49,7 @@ "@types/lodash": "4.14.202", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", + "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", diff --git a/src/components/username.tsx b/src/components/username.tsx index 2945126..404e4cb 100644 --- a/src/components/username.tsx +++ b/src/components/username.tsx @@ -1,9 +1,9 @@ -import { Typography, IconButton, Box, useTheme } from '@mui/material' +import { Box, IconButton, Typography, useTheme } from '@mui/material' import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { getProfileRoute } from '../routes' import { State } from '../store/rootReducer' import { hexToNpub } from '../utils' -import { Link } from 'react-router-dom' -import { getProfileRoute } from '../routes' type Props = { username: string @@ -60,6 +60,7 @@ type UserProps = { */ export const UserComponent = ({ pubkey, name, image }: UserProps) => { const theme = useTheme() + const navigate = useNavigate() const npub = hexToNpub(pubkey) const roboImage = `https://robohash.org/${npub}.png?set=set3` @@ -67,6 +68,10 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => { return ( { + e.stopPropagation() + navigate(getProfileRoute(pubkey)) + }} > { borderColor: `#${pubkey.substring(0, 6)}` }} /> - - - {name} - - + + {name} + ) } diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 301f21d..613ec44 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -46,6 +46,7 @@ export class MetadataController extends EventEmitter { const pool = new SimplePool() + // todo: use nostrController to get event // Try to get the metadata event from a special relay (wss://purplepag.es) const metadataEvent = await pool .get([this.specialMetadataRelay], eventFilter) @@ -68,6 +69,7 @@ export class MetadataController extends EventEmitter { } } + // todo use nostr controller to find event from connected relays // If no valid metadata event is found from the special relay, get the most popular relays const mostPopularRelays = await this.nostrController.getMostPopularRelays() @@ -141,6 +143,22 @@ export class MetadataController extends EventEmitter { } public findRelayListMetadata = async (hexKey: string) => { + let relayEvent: Event | null = null + + // Attempt to retrieve the metadata event from the local cache + const cachedRelayListMetadataEvent = + await localCache.getUserRelayListMetadata(hexKey) + + if (cachedRelayListMetadataEvent) { + const oneWeekInMS = 7 * 24 * 60 * 60 * 1000 // Number of milliseconds in one week + + // Check if the cached event is not older than one week + if (Date.now() - cachedRelayListMetadataEvent.cachedAt < oneWeekInMS) { + relayEvent = cachedRelayListMetadataEvent.event + } + } + + // define filter for relay list const eventFilter: Filter = { kinds: [kinds.RelayList], authors: [hexKey] @@ -148,19 +166,38 @@ export class MetadataController extends EventEmitter { const pool = new SimplePool() - let relayEvent = await pool - .get([this.specialMetadataRelay], eventFilter) - .catch((err) => { - console.error(err) - return null - }) + // Try to get the relayList event from a special relay (wss://purplepag.es) + if (!relayEvent) { + relayEvent = await pool + .get([this.specialMetadataRelay], eventFilter) + .then((event) => { + if (event) { + // update the event in local cache + localCache.addUserRelayListMetadata(event) + } + return event + }) + .catch((err) => { + console.error(err) + return null + }) + } if (!relayEvent) { + // If no valid relayList event is found from the special relay, get the most popular relays const mostPopularRelays = await this.nostrController.getMostPopularRelays() + // Query the most popular relays for relayList event relayEvent = await pool .get(mostPopularRelays, eventFilter) + .then((event) => { + if (event) { + // update the event in local cache + localCache.addUserRelayListMetadata(event) + } + return event + }) .catch((err) => { console.error(err) return null diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index ecc9cc9..65c9897 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -2,46 +2,50 @@ import NDK, { NDKEvent, NDKNip46Signer, NDKPrivateKeySigner, + NDKSubscription, NDKUser, - NostrEvent, - NDKSubscription + NostrEvent } from '@nostr-dev-kit/ndk' +import axios from 'axios' import { Event, EventTemplate, - SimplePool, - UnsignedEvent, Filter, Relay, + SimplePool, + UnsignedEvent, finalizeEvent, + kinds, nip04, nip19, - kinds + nip44 } from 'nostr-tools' +import { toast } from 'react-toastify' import { EventEmitter } from 'tseep' import { - updateNsecbunkerPubkey, setMostPopularRelaysAction, + setRelayConnectionStatusAction, setRelayInfoAction, - setRelayConnectionStatusAction + updateNsecbunkerPubkey } from '../store/actions' import { AuthState, LoginMethods } from '../store/auth/types' import store from '../store/store' import { - SignedEvent, - RelayMap, - RelayStats, - RelayReadStats, - RelayInfoObject, + RelayConnectionState, RelayConnectionStatus, - RelayConnectionState + RelayInfoObject, + RelayMap, + RelayReadStats, + RelayStats, + Rumor, + SignedEvent } from '../types' import { compareObjects, getNsecBunkerDelegatedKey, + randomNow, verifySignedEvent } from '../utils' -import axios from 'axios' export class NostrController extends EventEmitter { private static instance: NostrController @@ -56,7 +60,8 @@ export class NostrController extends EventEmitter { } private getNostrObject = () => { - if (window.nostr) return window.nostr + // fix: this is not picking up type declaration from src/system/index.d.ts + if (window.nostr) return window.nostr as any throw new Error( `window.nostr object not present. Make sure you have an nostr extension installed/working properly.` @@ -260,6 +265,157 @@ export class NostrController extends EventEmitter { return publishedRelays } + /** + * Asynchronously retrieves an event from a set of relays based on a provided filter. + * If no relays are specified, it defaults to using connected relays. + * + * @param {Filter} filter - The filter criteria to find the event. + * @param {string[]} [relays] - An optional array of relay URLs to search for the event. + * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. + */ + getEvent = async ( + filter: Filter, + relays?: string[] + ): Promise => { + // If no relays are provided or the provided array is empty, use connected relays if available. + if (!relays || relays.length === 0) { + relays = this.connectedRelays + ? this.connectedRelays.map((relay) => relay.url) + : [] + } + + // If still no relays are available, reject the promise with an error message. + if (relays.length === 0) { + return Promise.reject('Provide some relays to find the event') + } + + // Create a new instance of SimplePool to handle the relay connections and event retrieval. + const pool = new SimplePool() + + // Attempt to retrieve the event from the specified relays using the filter criteria. + const event = await pool.get(relays, filter).catch((err) => { + // Log any errors that occur during the event retrieval process. + console.log('An error occurred in finding the event', err) + // Show an error toast notification to the user. + toast.error('An error occurred in finding the event') + // Return null if an error occurs, indicating that no event was found. + return null + }) + + // Return the found event, or null if an error occurred. + return event + } + + createSeal = async (rumor: Rumor, receiver: string) => { + const encryptedContent = await this.nip44Encrypt( + receiver, + JSON.stringify(rumor) + ) + + const unsignedEvent: UnsignedEvent = { + kind: 13, + content: encryptedContent, + created_at: randomNow(), + tags: [], + pubkey: (store.getState().auth as AuthState).usersPubkey! + } + + const signedEvent = await this.signEvent(unsignedEvent) + return signedEvent + } + + nip44Encrypt = async (receiver: string, content: string) => { + const loginMethod = (store.getState().auth as AuthState).loginMethod + + if (loginMethod === LoginMethods.extension) { + const nostr = this.getNostrObject() + + if (!nostr.nip44) { + throw new Error( + `Your nostr extension does not support nip44 encryption & decryption` + ) + } + + const encrypted = await nostr.nip44.encrypt(receiver, content) + return encrypted as string + } + + if (loginMethod === LoginMethods.privateKey) { + const keys = (store.getState().auth as AuthState).keyPair + + if (!keys) { + throw new Error( + `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` + ) + } + + const { private: nsec } = keys + const privateKey = nip19.decode(nsec).data as Uint8Array + + const nip44ConversationKey = nip44.v2.utils.getConversationKey( + privateKey, + receiver + ) + const encrypted = nip44.v2.encrypt(content, nip44ConversationKey) + + return encrypted + } + + if (loginMethod === LoginMethods.nsecBunker) { + throw new Error( + `nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'` + ) + } + + throw new Error('Login method is undefined') + } + + nip44Decrypt = async (sender: string, content: string) => { + const loginMethod = (store.getState().auth as AuthState).loginMethod + + if (loginMethod === LoginMethods.extension) { + const nostr = this.getNostrObject() + + if (!nostr.nip44) { + throw new Error( + `Your nostr extension does not support nip44 encryption & decryption` + ) + } + + const decrypted = await nostr.nip44.decrypt(sender, content) + return decrypted as string + } + + if (loginMethod === LoginMethods.privateKey) { + const keys = (store.getState().auth as AuthState).keyPair + + if (!keys) { + throw new Error( + `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` + ) + } + + const { private: nsec } = keys + const privateKey = nip19.decode(nsec).data as Uint8Array + + const nip44ConversationKey = nip44.v2.utils.getConversationKey( + privateKey, + sender + ) + const decrypted = nip44.v2.decrypt(content, nip44ConversationKey) + + return decrypted + } + + if (loginMethod === LoginMethods.nsecBunker) { + throw new Error( + `nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'` + ) + } + + throw new Error('Login method is undefined') + } + /** * Signs an event with private key (if it is present in local storage) or * with browser extension (if it is present) or @@ -330,7 +486,7 @@ export class NostrController extends EventEmitter { } } - nip04Encrypt = async (receiver: string, content: string) => { + nip04Encrypt = async (receiver: string, content: string): Promise => { const loginMethod = (store.getState().auth as AuthState).loginMethod if (loginMethod === LoginMethods.extension) { @@ -385,7 +541,7 @@ export class NostrController extends EventEmitter { * @param content - The encrypted content to decrypt. * @returns A promise that resolves to the decrypted content. */ - nip04Decrypt = async (sender: string, content: string) => { + nip04Decrypt = async (sender: string, content: string): Promise => { const loginMethod = (store.getState().auth as AuthState).loginMethod if (loginMethod === LoginMethods.extension) { diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index c63e74d..2c13cf2 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -10,7 +10,8 @@ import { clearState, getRoboHashPicture, loadState, - saveNsecBunkerDelegatedKey + saveNsecBunkerDelegatedKey, + subscribeForSigits } from '../utils' import { LoadingSpinner } from '../components/LoadingSpinner' import { Dispatch } from '../store/store' @@ -100,6 +101,8 @@ export const MainLayout = () => { if (pubkey) { dispatch(setUserRobotImage(getRoboHashPicture(pubkey))) + + subscribeForSigits(pubkey) } } }, [authState]) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 84ad3bf..962afd6 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -18,12 +18,18 @@ import { Tooltip, Typography } from '@mui/material' +import type { Identifier, XYCoord } from 'dnd-core' +import saveAs from 'file-saver' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { useEffect, useRef, useState } from 'react' +import { Event, kinds } from 'nostr-tools' +import { useEffect, useMemo, useRef, useState } from 'react' +import { DndProvider, useDrag, useDrop } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' import { useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' +import { v4 as uuidV4 } from 'uuid' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserComponent } from '../../components/username' import { MetadataController, NostrController } from '../../controllers' @@ -33,24 +39,21 @@ import { Meta, ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, generateEncryptionKey, + generateKeys, generateKeysFile, getHash, hexToNpub, isOnline, + now, npubToHex, queryNip05, - sendDM, + sendNotification, shorten, signEventForMetaFile, + updateUsersAppData, uploadToFileStorage } from '../../utils' import styles from './style.module.scss' -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' -import type { Identifier, XYCoord } from 'dnd-core' -import { useDrag, useDrop } from 'react-dnd' -import saveAs from 'file-saver' -import { Event, kinds } from 'nostr-tools' export const CreatePage = () => { const navigate = useNavigate() @@ -75,6 +78,15 @@ export const CreatePage = () => { const nostrController = NostrController.getInstance() + // Set up event listener for authentication event + nostrController.on('nsecbunker-auth', (url) => { + setAuthUrl(url) + }) + + const uuid = useMemo(() => { + return uuidV4() + }, []) + useEffect(() => { if (uploadedFile) { setSelectedFiles([uploadedFile]) @@ -283,16 +295,28 @@ export const CreatePage = () => { return fileHashes } - // Create a zip file with the selected files and sign the event - const createZipFile = async (fileHashes: { - [key: string]: string - }): Promise<{ zip: JSZip; createSignature: string } | null> => { + // initialize a zip file with the selected files and generate creator's signature + const initZipFileAndCreatorSignature = async ( + encryptionKey: string, + fileHashes: { + [key: string]: string + } + ): Promise<{ zip: JSZip; createSignature: string } | null> => { const zip = new JSZip() selectedFiles.forEach((file) => { zip.file(`files/${file.name}`, file) }) + // generate key pairs for decryption + const pubkeys = users.map((user) => user.pubkey) + // also add creator in the list + if (pubkeys.includes(usersPubkey!)) { + pubkeys.push(usersPubkey!) + } + + const keys = await generateKeys(pubkeys, encryptionKey) + const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) @@ -302,7 +326,8 @@ export const CreatePage = () => { JSON.stringify({ signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), - fileHashes + fileHashes, + keys }), nostrController, setIsLoading @@ -320,38 +345,6 @@ export const CreatePage = () => { } } - // Add metadata and file hashes to the zip file - const addMetaToZip = async ( - zip: JSZip, - createSignature: string - ): Promise => { - // create content for meta file - const meta: Meta = { - title, - createSignature, - docSignatures: {} - } - - try { - const stringifiedMeta = JSON.stringify(meta, null, 2) - zip.file('meta.json', stringifiedMeta) - - const metaHash = await getHash(stringifiedMeta) - if (!metaHash) return null - - const metaHashJson = { - [usersPubkey!]: metaHash - } - - zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2)) - return metaHash - } catch (err) { - console.error(err) - toast.error('An error occurred in converting meta json to string') - return null - } - } - // Handle errors during zip file generation const handleZipError = (err: any) => { console.log('Error in zip:>> ', err) @@ -390,7 +383,7 @@ export const CreatePage = () => { encryptionKey: string ): Promise => { // Get the current timestamp in seconds - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() const blob = new Blob([encryptedArrayBuffer]) // Create a File object with the Blob data const file = new File([blob], `compressed.sigit`, { @@ -432,9 +425,9 @@ export const CreatePage = () => { const handleOnlineFlow = async ( encryptedArrayBuffer: ArrayBuffer, - encryptionKey: string + meta: Meta ) => { - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() const blob = new Blob([encryptedArrayBuffer]) // Create a File object with the Blob data const file = new File([blob], `compressed-${unixNow}.sigit`, { @@ -444,7 +437,12 @@ export const CreatePage = () => { const fileUrl = await uploadFile(file) if (!fileUrl) return - await sendDMs(fileUrl, encryptionKey) + const updatedEvent = await updateUsersAppData(fileUrl, meta) + if (!updatedEvent) return + + await sendDMs(fileUrl, meta) + + navigate(appPrivateRoutes.sign, { state: { sigit: { fileUrl, meta } } }) } // Handle errors during file upload @@ -471,33 +469,21 @@ export const CreatePage = () => { } // Send DMs to signers and viewers with the file URL - const sendDMs = async (fileUrl: string, encryptionKey: string) => { + const sendDMs = async (fileUrl: string, meta: Meta) => { setLoadingSpinnerDesc('Sending DM to signers/viewers') const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) - if (signers.length > 0) { - await sendDM( - fileUrl, - encryptionKey, - signers[0].pubkey, - nostrController, - true, - setAuthUrl - ) - } else { - for (const viewer of viewers) { - await sendDM( - fileUrl, - encryptionKey, - viewer.pubkey, - nostrController, - false, - setAuthUrl - ) - } - } + const receivers = + signers.length > 0 + ? [signers[0].pubkey] + : viewers.map((viewer) => viewer.pubkey) + + const promises = receivers.map((receiver) => + sendNotification(receiver, meta, fileUrl) + ) + await Promise.allSettled(promises) } // Manage offline scenarios for signing or viewing the file @@ -524,21 +510,30 @@ export const CreatePage = () => { const fileHashes = await generateFileHashes() if (!fileHashes) return - const createZipResponse = await createZipFile(fileHashes) + const encryptionKey = await generateEncryptionKey() + + const createZipResponse = await initZipFileAndCreatorSignature( + encryptionKey, + fileHashes + ) if (!createZipResponse) return const { zip, createSignature } = createZipResponse - const metaHash = await addMetaToZip(zip, createSignature) - if (!metaHash) return + // create content for meta file + const meta: Meta = { + uuid, + title, + modifiedAt: now(), + createSignature, + docSignatures: {} + } setLoadingSpinnerDesc('Generating zip file') const arrayBuffer = await generateZipFile(zip) if (!arrayBuffer) return - const encryptionKey = await generateEncryptionKey() - setLoadingSpinnerDesc('Encrypting zip file') const encryptedArrayBuffer = await encryptZipFile( arrayBuffer, @@ -546,12 +541,11 @@ export const CreatePage = () => { ) if (await isOnline()) { - await handleOnlineFlow(encryptedArrayBuffer, encryptionKey) + await handleOnlineFlow(encryptedArrayBuffer, meta) } else { + // todo: fix offline flow await handleOfflineFlow(encryptedArrayBuffer, encryptionKey) } - - navigate(appPrivateRoutes.sign, { state: { arrayBuffer } }) } if (authUrl) { diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 181b67d..30d39db 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,21 +1,59 @@ -import { - Add, - CalendarMonth, - Description, - PersonOutline, - Upload -} from '@mui/icons-material' +import { Add, CalendarMonth, Description, Upload } from '@mui/icons-material' import { Box, Button, Tooltip, Typography } from '@mui/material' +import JSZip from 'jszip' +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' import { appPrivateRoutes, appPublicRoutes } from '../../routes' import styles from './style.module.scss' -import { useRef } from 'react' -import JSZip from 'jszip' -import { toast } from 'react-toastify' +import { MetadataController, NostrController } from '../../controllers' +import { + formatTimestamp, + getUsersAppData, + hexToNpub, + npubToHex, + parseJson, + shorten +} from '../../utils' +import { LoadingSpinner } from '../../components/LoadingSpinner' +import { + CreateSignatureEventContent, + Meta, + ProfileMetadata, + Sigit +} from '../../types' +import { Event, kinds, verifyEvent } from 'nostr-tools' +import { UserComponent } from '../../components/username' export const HomePage = () => { const navigate = useNavigate() const fileInputRef = useRef(null) + const [isLoading, setIsLoading] = useState(true) + const [loadingSpinnerDesc] = useState(`Finding user's app data`) + const [authUrl, setAuthUrl] = useState() + const [sigits, setSigits] = useState([]) + + const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( + {} + ) + + useEffect(() => { + const nostrController = NostrController.getInstance() + // Set up event listener for authentication event + nostrController.on('nsecbunker-auth', (url) => { + setAuthUrl(url) + }) + + getUsersAppData() + .then((res) => { + if (res) { + setSigits(Object.values(res)) + } + }) + .finally(() => { + setIsLoading(false) + }) + }, []) const handleUploadClick = () => { if (fileInputRef.current) { @@ -63,81 +101,193 @@ export const HomePage = () => { } } + if (authUrl) { + return ( +