diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 8c7ac63..aaabbd3 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -37,7 +37,7 @@ import { setUserRobotImage } from '../../store/userRobotImage/action' import { Container } from '../Container' import { ButtonIcon } from '../ButtonIcon' -const metadataController = new MetadataController() +const metadataController = MetadataController.getInstance() export const AppBar = () => { const navigate = useNavigate() diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 473a942..62f397c 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,7 +1,7 @@ import { Meta } from '../../types' import { SigitCardDisplayInfo, SigitStatus, SignStatus } from '../../utils' import { Link } from 'react-router-dom' -import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' +import { formatTimestamp, npubToHex } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { Button, Divider, Tooltip } from '@mui/material' import { DisplaySigner } from '../DisplaySigner' @@ -17,9 +17,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' -import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' -import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { useSigitMeta } from '../../hooks/useSigitMeta' import { extractFileExtensions } from '../../utils/file' @@ -33,12 +31,6 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { parsedMeta const { signersStatus, fileHashes } = useSigitMeta(meta) - - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers - ]) - const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) return ( @@ -54,62 +46,29 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { >

{title}

- {submittedBy && - (function () { - const profile = profiles[submittedBy] - return ( - - - - - - ) - })()} + {submittedBy && ( + + )} {submittedBy && signers.length ? ( ) : null} {signers.map((signer) => { const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] - return ( - - - - - + ) })}
-
+
{createdAt ? formatTimestamp(createdAt) : null}
diff --git a/src/components/DisplaySigner/index.tsx b/src/components/DisplaySigner/index.tsx index 63aa154..e05dcae 100644 --- a/src/components/DisplaySigner/index.tsx +++ b/src/components/DisplaySigner/index.tsx @@ -1,5 +1,4 @@ import { Badge } from '@mui/material' -import { ProfileMetadata } from '../../types' import styles from './style.module.scss' import { UserAvatar } from '../UserAvatar' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -15,38 +14,33 @@ import { SignStatus } from '../../utils' import { Spinner } from '../Spinner' type DisplaySignerProps = { - profile: ProfileMetadata pubkey: string status: SignStatus } -export const DisplaySigner = ({ - status, - profile, - pubkey -}: DisplaySignerProps) => { - const getStatusIcon = (status: SignStatus) => { - switch (status) { - case SignStatus.Signed: - return - case SignStatus.Awaiting: - return ( - - - - ) - case SignStatus.Pending: - return - case SignStatus.Invalid: - return - case SignStatus.Viewer: - return +const getStatusIcon = (status: SignStatus) => { + switch (status) { + case SignStatus.Signed: + return + case SignStatus.Awaiting: + return ( + + + + ) + case SignStatus.Pending: + return + case SignStatus.Invalid: + return + case SignStatus.Viewer: + return - default: - return - } + default: + return } +} +export const DisplaySigner = ({ status, pubkey }: DisplaySignerProps) => { return ( {getStatusIcon(status)}
} > - + ) } diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index 6049a07..0ea1fc1 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -3,34 +3,56 @@ import { getProfileRoute } from '../../routes' import styles from './styles.module.scss' import { AvatarIconButton } from '../UserAvatarIconButton' import { Link } from 'react-router-dom' +import { useProfileMetadata } from '../../hooks/useProfileMetadata' +import { Tooltip } from '@mui/material' +import { shorten } from '../../utils' +import { TooltipChild } from '../TooltipChild' interface UserAvatarProps { - name?: string pubkey: string - image?: string + isNameVisible?: boolean } /** * This component will be used for the displaying username and profile picture. * Clicking will navigate to the user's profile. */ -export const UserAvatar = ({ pubkey, name, image }: UserAvatarProps) => { +export const UserAvatar = ({ + pubkey, + isNameVisible = false +}: UserAvatarProps) => { + const profile = useProfileMetadata(pubkey) + const name = profile?.display_name || profile?.name || shorten(pubkey) + const image = profile?.picture + return ( - - {name ? {name} : null} + + + + + + {isNameVisible && name ? ( + {name} + ) : null} ) } diff --git a/src/components/UsersDetails.tsx/index.tsx b/src/components/UsersDetails.tsx/index.tsx index bddae82..3a5c358 100644 --- a/src/components/UsersDetails.tsx/index.tsx +++ b/src/components/UsersDetails.tsx/index.tsx @@ -1,11 +1,9 @@ import { Divider, Tooltip } from '@mui/material' -import { useSigitProfiles } from '../../hooks/useSigitProfiles' import { formatTimestamp, fromUnixTimestamp, hexToNpub, npubToHex, - shorten, SignStatus } from '../../utils' import { useSigitMeta } from '../../hooks/useSigitMeta' @@ -24,10 +22,10 @@ import { import { getExtensionIconLabel } from '../getExtensionIconLabel' import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' -import { TooltipChild } from '../TooltipChild' import { DisplaySigner } from '../DisplaySigner' import { Meta } from '../../types' import { extractFileExtensions } from '../../utils/file' +import { UserAvatar } from '../UserAvatar' interface UsersDetailsProps { meta: Meta @@ -36,6 +34,7 @@ interface UsersDetailsProps { export const UsersDetails = ({ meta }: UsersDetailsProps) => { const { submittedBy, + exportedBy, signers, viewers, fileHashes, @@ -47,11 +46,6 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { isValid } = useSigitMeta(meta) const { usersPubkey } = useSelector((state: State) => state.auth) - const profiles = useSigitProfiles([ - ...(submittedBy ? [submittedBy] : []), - ...signers, - ...viewers - ]) const userCanSign = typeof usersPubkey !== 'undefined' && signers.includes(hexToNpub(usersPubkey)) @@ -63,31 +57,12 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {

Signers

- {submittedBy && - (function () { - const profile = profiles[submittedBy] - return ( - - - - - - ) - })()} + {submittedBy && ( + + )} {submittedBy && signers.length ? ( @@ -96,26 +71,12 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { {signers.map((signer) => { const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] - return ( - - - - - + ) })} @@ -128,34 +89,28 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => { {viewers.map((signer) => { const pubkey = npubToHex(signer)! - const profile = profiles[pubkey] return ( - - - - - + ) })}
)} + + {exportedBy && ( + <> +

Exported By

+
+ +
+ + )}

Details

diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 09b20df..cc8def5 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -25,7 +25,7 @@ export class AuthController { constructor() { this.nostrController = NostrController.getInstance() - this.metadataController = new MetadataController() + this.metadataController = MetadataController.getInstance() } /** diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 8053874..984afd3 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -22,6 +22,7 @@ import { import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const' export class MetadataController extends EventEmitter { + private static instance: MetadataController private nostrController: NostrController private specialMetadataRelay = 'wss://purplepag.es' private pendingFetches = new Map>() // Track pending fetches @@ -31,6 +32,13 @@ export class MetadataController extends EventEmitter { this.nostrController = NostrController.getInstance() } + public static getInstance(): MetadataController { + if (!MetadataController.instance) { + MetadataController.instance = new MetadataController() + } + return MetadataController.instance + } + /** * Asynchronously checks for more recent metadata events authored by a specific key. * If a more recent metadata event is found, it is handled and returned. @@ -119,7 +127,6 @@ export class MetadataController extends EventEmitter { // Check if the cached metadata is older than one day if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) { // If older than one week, find the metadata from relays in background - this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event) } diff --git a/src/hooks/useProfileMetadata.tsx b/src/hooks/useProfileMetadata.tsx new file mode 100644 index 0000000..f746f0d --- /dev/null +++ b/src/hooks/useProfileMetadata.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { ProfileMetadata } from '../types/profile' +import { MetadataController } from '../controllers/MetadataController' +import { Event, kinds } from 'nostr-tools' + +export const useProfileMetadata = (pubkey: string) => { + const [profileMetadata, setProfileMetadata] = useState() + + useEffect(() => { + const metadataController = MetadataController.getInstance() + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) { + setProfileMetadata(metadataContent) + } + } + + if (pubkey) { + metadataController.on(pubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + + metadataController + .findMetadata(pubkey) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(metadataEvent) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${pubkey}`, + err + ) + }) + } + + return () => { + metadataController.off(pubkey, handleMetadataEvent) + } + }, [pubkey]) + + return profileMetadata +} diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 088940e..22a6a6d 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -33,6 +33,10 @@ export interface FlatMeta // Remove pubkey and use submittedBy as `npub1${string}` submittedBy?: `npub1${string}` + // Optional field only present on exported sigits + // Exporting adds user's pubkey + exportedBy?: `npub1${string}` + // Remove created_at and replace with createdAt createdAt?: number @@ -68,6 +72,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [tags, setTags] = useState() const [createdAt, setCreatedAt] = useState() const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event + const [exportedBy, setExportedBy] = useState<`npub1${string}`>() // pubkey from export signature nostr event const [id, setId] = useState() const [sig, setSig] = useState() @@ -99,6 +104,18 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { if (!meta) return ;(async function () { try { + if (meta.exportSignature) { + const exportSignatureEvent = await parseNostrEvent( + meta.exportSignature + ) + if ( + verifyEvent(exportSignatureEvent) && + exportSignatureEvent.pubkey + ) { + setExportedBy(exportSignatureEvent.pubkey as `npub1${string}`) + } + } + const createSignatureEvent = await parseNostrEvent(meta.createSignature) const { kind, tags, created_at, pubkey, id, sig, content } = @@ -265,6 +282,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { tags, createdAt, submittedBy, + exportedBy, id, sig, signers, diff --git a/src/hooks/useSigitProfiles.tsx b/src/hooks/useSigitProfiles.tsx deleted file mode 100644 index 88d6c50..0000000 --- a/src/hooks/useSigitProfiles.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useState } from 'react' -import { ProfileMetadata } from '../types' -import { MetadataController } from '../controllers' -import { npubToHex } from '../utils' -import { Event, kinds } from 'nostr-tools' - -/** - * Extracts profiles from metadata events - * @param pubkeys Array of npubs to check - * @returns ProfileMetadata - */ -export const useSigitProfiles = ( - pubkeys: `npub1${string}`[] -): { [key: string]: ProfileMetadata } => { - const [profileMetadata, setProfileMetadata] = useState<{ - [key: string]: ProfileMetadata - }>({}) - - useEffect(() => { - if (pubkeys.length) { - const metadataController = new MetadataController() - - // Remove duplicate keys - const users = new Set([...pubkeys]) - - const handleMetadataEvent = (key: string) => (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) { - setProfileMetadata((prev) => ({ - ...prev, - [key]: metadataContent - })) - } - } - - users.forEach((user) => { - const hexKey = npubToHex(user) - if (hexKey && !(hexKey in profileMetadata)) { - metadataController.on(hexKey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(hexKey)(event) - } - }) - - metadataController - .findMetadata(hexKey) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent) - }) - .catch((err) => { - console.error( - `error occurred in finding metadata for: ${user}`, - err - ) - }) - } - }) - - return () => { - users.forEach((key) => { - metadataController.off(key, handleMetadataEvent(key)) - }) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pubkeys]) - - return profileMetadata -} diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index 9402e97..3a1f10e 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -38,7 +38,7 @@ export const MainLayout = () => { const hasSubscribed = useRef(false) useEffect(() => { - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const logout = () => { dispatch( diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index bdfa958..f4dc550 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -36,7 +36,6 @@ import { npubToHex, queryNip05, sendNotification, - shorten, signEventForMetaFile, updateUsersAppData, uploadToFileStorage @@ -113,6 +112,8 @@ export const CreatePage = () => { const [error, setError] = useState() const [users, setUsers] = useState([]) + const signers = users.filter((u) => u.role === UserRole.signer) + const viewers = users.filter((u) => u.role === UserRole.viewer) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) @@ -252,7 +253,7 @@ export const CreatePage = () => { useEffect(() => { users.forEach((user) => { if (!(user.pubkey in metadata)) { - const metadataController = new MetadataController() + const metadataController = MetadataController.getInstance() const handleMetadataEvent = (event: Event) => { const metadataContent = @@ -647,6 +648,11 @@ export const CreatePage = () => { } saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`) + + // If user is the next signer, we can navigate directly to sign page + if (signers[0].pubkey === usersPubkey) { + navigate(appPrivateRoutes.sign, { state: { uploadedZip: finalZipFile } }) + } setIsLoading(false) } @@ -672,9 +678,6 @@ export const CreatePage = () => { }, zipUrl: string ) => { - const signers = users.filter((user) => user.role === UserRole.signer) - const viewers = users.filter((user) => user.role === UserRole.viewer) - const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), @@ -703,9 +706,6 @@ export const CreatePage = () => { // Send notifications to signers and viewers const sendNotifications = (meta: Meta) => { - const signers = users.filter((user) => user.role === UserRole.signer) - const viewers = users.filter((user) => user.role === UserRole.viewer) - // no need to send notification to self so remove it from the list const receivers = ( signers.length > 0 @@ -787,7 +787,7 @@ export const CreatePage = () => { toast.error('Failed to publish notifications') }) - navigate(appPrivateRoutes.sign, { state: { meta: meta } }) + navigate(appPrivateRoutes.sign, { state: { meta } }) } else { const zip = new JSZip() @@ -914,7 +914,6 @@ export const CreatePage = () => {
{ } type DisplayUsersProps = { - metadata: { [key: string]: ProfileMetadata } users: User[] handleUserRoleChange: (role: UserRole, pubkey: string) => void handleRemoveUser: (pubkey: string) => void @@ -1019,7 +1017,6 @@ type DisplayUsersProps = { } const DisplayUser = ({ - metadata, users, handleUserRoleChange, handleRemoveUser, @@ -1033,7 +1030,6 @@ const DisplayUser = ({ .map((user, index) => ( void handleRemoveUser: (pubkey: string) => void @@ -1079,7 +1073,6 @@ type SignerCounterpartProps = CounterpartProps & { } const SignerCounterpart = ({ - userMeta, user, index, moveSigner, @@ -1172,7 +1165,6 @@ const SignerCounterpart = ({ @@ -1181,7 +1173,6 @@ const SignerCounterpart = ({ } const Counterpart = ({ - userMeta, user, handleUserRoleChange, handleRemoveUser @@ -1189,15 +1180,7 @@ const Counterpart = ({ return ( <>
- +