diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 897ebec..e28f698 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -28,12 +28,12 @@ import { import { appPrivateRoutes } from '../../routes' import { CreateSignatureEventContent, + KeyboardCode, Meta, ProfileMetadata, SignedEvent, User, - UserRole, - KeyboardCode + UserRole } from '../../types' import { encryptArrayBuffer, @@ -52,8 +52,7 @@ import { updateUsersAppData, uploadToFileStorage, DEFAULT_TOOLBOX, - settleAllFullfilfedPromises, - debounceCustom + settleAllFullfilfedPromises } from '../../utils' import { Container } from '../../components/Container' import fileListStyles from '../../components/FileList/style.module.scss' @@ -79,12 +78,10 @@ import { getSigitFile, SigitFile } from '../../utils/file.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts' import { Autocomplete } from '@mui/lab' import _, { truncate } from 'lodash' -import nostrDefaultImage from '../../assets/images/nostr-logo.png' import * as React from 'react' +import { AvatarIconButton } from '../../components/UserAvatarIconButton' -interface FoundUsers extends Event { - npub: string -} +type FoundUser = Event & { npub: string } export const CreatePage = () => { const navigate = useNavigate() @@ -126,19 +123,26 @@ export const CreatePage = () => { const [drawnFiles, setDrawnFiles] = useState([]) const [parsingPdf, setIsParsing] = useState(false) + const searchFieldRef = useRef(null) + const [selectedTool, setSelectedTool] = useState() - const [foundUsers, setFoundUsers] = useState([]) + const [foundUsers, setFoundUsers] = useState([]) const [searchUsersLoading, setSearchUsersLoading] = useState(false) + const [pastedUserNpubOrNip05, setPastedUserNpubOrNip05] = useState< + string | undefined + >() /** * Fired when user select */ const handleSearchUserChange = useCallback( - (_event: React.SyntheticEvent, value: string | FoundUsers | null) => { + (_event: React.SyntheticEvent, value: string | FoundUser | null) => { if (typeof value === 'object') { - const ndkEvent = value as FoundUsers - setUserInput(hexToNpub(ndkEvent.pubkey)) + const ndkEvent = value as FoundUser + if (ndkEvent?.pubkey) { + setUserInput(hexToNpub(ndkEvent.pubkey)) + } } }, [setUserInput] @@ -166,13 +170,36 @@ export const CreatePage = () => { [...relaySet.write] ) .then((events) => { - const fineFilteredEvents = events - .filter((event) => event.content.includes(`"name":"${searchTerm}`)) - .map((event) => ({ - ...event, - npub: hexToNpub(event.pubkey) - })) + console.log('events', events) + const fineFilteredEvents: FoundUser[] = events + .filter((event) => { + const lowercaseContent = event.content.toLowerCase() + + return ( + lowercaseContent.includes( + `"name":"${searchTerm.toLowerCase()}"` + ) || + lowercaseContent.includes( + `"display_name":"${searchTerm.toLowerCase()}"` + ) || + lowercaseContent.includes( + `"username":"${searchTerm.toLowerCase()}"` + ) || + lowercaseContent.includes(`"nip05":"${searchTerm.toLowerCase()}"`) + ) + }) + .reduce((uniqueEvents: FoundUser[], event: Event) => { + if (!uniqueEvents.some((e: Event) => e.pubkey === event.pubkey)) { + uniqueEvents.push({ + ...event, + npub: hexToNpub(event.pubkey) + }) + } + return uniqueEvents + }, []) + + console.log('fineFilteredEvents', fineFilteredEvents) setFoundUsers(fineFilteredEvents) }) .catch((error) => { @@ -183,13 +210,16 @@ export const CreatePage = () => { }) } - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedHandleSearchUsers = useCallback( - debounceCustom((value: string) => { - if (foundUsers.length === 0) handleSearchUsers(value) - }, 1000), - [] - ) + useEffect(() => { + setTimeout(() => { + if (foundUsers.length) { + if (searchFieldRef.current) { + searchFieldRef.current.blur() + searchFieldRef.current.focus() + } + } + }) + }, [foundUsers]) const handleInputKeyDown = (event: React.KeyboardEvent) => { if ( @@ -197,7 +227,17 @@ export const CreatePage = () => { event.code === KeyboardCode.NumpadEnter ) { event.preventDefault() - handleAddUser() + + // If pasted user npub of nip05 is present, we just add the user to the counterparts list + if (pastedUserNpubOrNip05) { + setUserInput(pastedUserNpubOrNip05) + setPastedUserNpubOrNip05(undefined) + } else { + // Otherwize if search already provided some results, user must manually click the search button + if (!foundUsers.length) { + handleSearchUsers() + } + } } } @@ -296,7 +336,7 @@ export const CreatePage = () => { } }, [usersPubkey]) - const handleAddUser = async () => { + const handleAddUser = useCallback(async () => { setError(undefined) const addUser = (pubkey: string) => { @@ -338,6 +378,8 @@ export const CreatePage = () => { const input = userInput.toLowerCase() + setUserSearchInput('') + if (input.startsWith('npub')) { return handleAddNpubUser(input) } @@ -387,7 +429,20 @@ export const CreatePage = () => { } return } - } + }, [ + userInput, + userRole, + setError, + setUsers, + setUserSearchInput, + setIsLoading, + setLoadingSpinnerDesc, + setUserInput + ]) + + useEffect(() => { + if (userInput?.length > 0) handleAddUser() + }, [handleAddUser, userInput]) const handleUserRoleChange = (role: UserRole, pubkey: string) => { setUsers((prevUsers) => @@ -876,10 +931,57 @@ export const CreatePage = () => { } } + /** + * Handles the user search textfield change + * If it's not valid npub or nip05, search will be automatically triggered + */ + const handleSearchAutocompleteTextfieldChange = async ( + e: React.ChangeEvent + ) => { + const value = e.target.value + + const disarmAddOnEnter = () => { + setPastedUserNpubOrNip05(undefined) + } + + // 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() + } + } else if (value.includes('@')) { + // Seems like it's nip05 format + const { pubkey } = await queryNip05(value).catch((err) => { + console.error(err) + return { pubkey: null } + }) + + if (pubkey) { + // Arm the manual user npub add after enter is hit, we don't want to trigger search + setPastedUserNpubOrNip05(hexToNpub(pubkey)) + } else { + disarmAddOnEnter() + } + } else { + // Disarm the add user on enter hit, and trigger search after 1 second + disarmAddOnEnter() + } + + setUserSearchInput(value) + } + const parseContent = (event: Event) => { try { return JSON.parse(event.content) } catch (e) { + return undefined console.error(e) } } @@ -951,18 +1053,25 @@ export const CreatePage = () => {
x} getOptionLabel={(option) => { - let label = (option as FoundUsers).npub + let label: string = (option as FoundUser).npub - const contentJson = parseContent(option as FoundUsers) - label = contentJson.name + const contentJson = parseContent(option as FoundUser) + + if (contentJson?.name) { + label = contentJson.name + } else { + label = option as string + } return label }} @@ -978,17 +1087,31 @@ export const CreatePage = () => { {...optionProps} key={option.pubkey} > - - (currentTarget.src = nostrDefaultImage) - } - alt="" + - {contentJson.name} ( - {truncate(option.npub, { length: 16 })}) + +
+ {contentJson.name}{' '} + {usersPubkey === option.pubkey ? ( + + Me + + ) : ( + '' + )}{' '} + ({truncate(option.npub, { length: 16 })}) +
) }} @@ -996,45 +1119,14 @@ export const CreatePage = () => { setUserSearchInput(e.target.value)} - onChange={(e) => { - const value = e.target.value - setUserSearchInput(value) - debouncedHandleSearchUsers(value) - }} + inputRef={searchFieldRef} + label="Add/Search counterpart" + onKeyDown={handleInputKeyDown} + onChange={handleSearchAutocompleteTextfieldChange} /> )} />
- -
- -
-
- setUserInput(e.target.value)} - onKeyDown={handleInputKeyDown} - disabled={searchUsersLoading} - error={!!error} - /> -
- + + {!pastedUserNpubOrNip05 ? ( + + ) : ( + + )}