Compare commits
6 Commits
feature/hi
...
feature/ed
Author | SHA1 | Date | |
---|---|---|---|
ba3775e6c6 | |||
4ad66c8711 | |||
6a04c3ec4b | |||
6d72cf1f82 | |||
d199dcf9f7 | |||
0f28c80a15 |
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { selectKeys } from '@/store'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import { ChangeEvent, Fragment, useCallback, useEffect, useState } from 'react'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { StyledSettingContainer } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
|
||||
export const ModalEditName = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
|
||||
const key = keys.find((k) => k.npub === npub)
|
||||
const name = key?.name || ''
|
||||
|
||||
const { palette } = useTheme()
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
|
||||
const [enteredName, setEnteredName] = useState('')
|
||||
const [debouncedName] = useDebounce(enteredName, 300)
|
||||
const isNameEqual = debouncedName === name
|
||||
|
||||
const [receiverNpub, setReceiverNpub] = useState('')
|
||||
|
||||
const [isAvailable, setIsAvailable] = useState(true)
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isTransferLoading, setIsTransferLoading] = useState(false)
|
||||
|
||||
const checkIsUsernameAvailable = useCallback(async () => {
|
||||
if (!debouncedName.trim().length) return undefined
|
||||
try {
|
||||
setIsChecking(true)
|
||||
const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`)
|
||||
setIsAvailable(!npubNip05 || npubNip05 === npub)
|
||||
setIsChecking(false)
|
||||
} catch (error) {
|
||||
setIsAvailable(true)
|
||||
setIsChecking(false)
|
||||
}
|
||||
}, [debouncedName])
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUsernameAvailable()
|
||||
}, [checkIsUsernameAvailable])
|
||||
|
||||
useEffect(() => {
|
||||
setEnteredName(name)
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
setEnteredName('')
|
||||
setReceiverNpub('')
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isModalOpened])
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setEnteredName(e.target.value)
|
||||
|
||||
const handleReceiverNpubChange = (e: ChangeEvent<HTMLInputElement>) => setReceiverNpub(e.target.value)
|
||||
|
||||
const getInputHelperText = () => {
|
||||
if (!debouncedName.trim().length || isNameEqual) return ''
|
||||
if (isChecking) return 'Loading...'
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<Fragment>
|
||||
<CheckmarkIcon /> Available
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
const getHelperTextColor = useCallback(() => {
|
||||
if (!debouncedName || isChecking || isNameEqual) return palette.textSecondaryDecorate.main
|
||||
return isAvailable ? palette.success.main : palette.error.main
|
||||
// deps
|
||||
}, [debouncedName, isAvailable, isChecking, isNameEqual, palette])
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length
|
||||
const isTransferButtonDisabled = !name.length || !receiverNpub.trim().length || isTransferLoading
|
||||
|
||||
const handleEditName = async () => {
|
||||
if (isEditButtonDisabled) return
|
||||
try {
|
||||
setIsLoading(true)
|
||||
await swicCall('editName', npub, enteredName)
|
||||
notify('Username updated!', 'success')
|
||||
setIsLoading(false)
|
||||
} catch (error: any) {
|
||||
setIsLoading(false)
|
||||
notify(error?.message || 'Failed to edit username!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTransferName = async () => {
|
||||
if (isTransferButtonDisabled) return
|
||||
try {
|
||||
setIsTransferLoading(true)
|
||||
await swicCall('transferName', npub, enteredName, receiverNpub)
|
||||
notify('Username transferred!', 'success')
|
||||
setIsTransferLoading(false)
|
||||
setEnteredName('')
|
||||
} catch (error: any) {
|
||||
setIsTransferLoading(false)
|
||||
notify(error?.message || 'Failed to transfer username!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} title="Username Settings" onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'}>
|
||||
<StyledSettingContainer>
|
||||
<SectionTitle>Change name</SectionTitle>
|
||||
<Input
|
||||
label="User name"
|
||||
fullWidth
|
||||
placeholder="Enter a Username"
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
helperText={inputHelperText}
|
||||
onChange={handleNameChange}
|
||||
value={enteredName}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: getHelperTextColor(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button fullWidth disabled={isEditButtonDisabled} onClick={handleEditName}>
|
||||
Save name {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</StyledSettingContainer>
|
||||
<StyledSettingContainer>
|
||||
<SectionTitle>Transfer name</SectionTitle>
|
||||
<Input
|
||||
label="Receiver npub"
|
||||
fullWidth
|
||||
placeholder="npub1..."
|
||||
onChange={handleReceiverNpubChange}
|
||||
value={receiverNpub}
|
||||
/>
|
||||
<Button fullWidth onClick={handleTransferName} disabled={isTransferButtonDisabled}>
|
||||
Transfer name
|
||||
</Button>
|
||||
</StyledSettingContainer>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Stack, StackProps, styled } from "@mui/material";
|
||||
|
||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||
))(({ theme }) => ({
|
||||
padding: '1rem',
|
||||
borderRadius: '1rem',
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
}))
|
@ -100,7 +100,7 @@ export const ModalLogin = () => {
|
||||
if (isPopup && isModalOpened) {
|
||||
swicCall('fetchPendingRequests', npub, appNpub)
|
||||
|
||||
fetchNpubNames(npub).then(names => {
|
||||
fetchNpubNames(npub).then((names) => {
|
||||
if (names.length) {
|
||||
setValue('username', `${names[0]}@${DOMAIN}`)
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ class Nip46Backend extends NDKNip46Backend {
|
||||
// }
|
||||
// }
|
||||
|
||||
// FIXME why do we need it? Just to print
|
||||
// FIXME why do we need it? Just to print
|
||||
// class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
// readonly backend: NDKNip46Backend
|
||||
// readonly method: string
|
||||
@ -508,6 +508,41 @@ export class NoauthBackend {
|
||||
throw new Error('Too many requests, retry later')
|
||||
}
|
||||
|
||||
private async sendDeleteNameToServer(npub: string, name: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
name,
|
||||
})
|
||||
|
||||
const method = 'DELETE'
|
||||
const url = `${NOAUTHD_URL}/name`
|
||||
|
||||
return this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private async sendTransferNameToServer(npub: string, name: string, newNpub: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
name,
|
||||
newNpub,
|
||||
})
|
||||
|
||||
const method = 'PUT'
|
||||
const url = `${NOAUTHD_URL}/name`
|
||||
|
||||
return this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private async sendTokenToServer(npub: string, token: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
@ -766,7 +801,7 @@ export class NoauthBackend {
|
||||
return // noop
|
||||
case DECISION.ALLOW:
|
||||
case DECISION.DISALLOW:
|
||||
// fall through
|
||||
// fall through
|
||||
}
|
||||
|
||||
const allow = decision === DECISION.ALLOW
|
||||
@ -1116,6 +1151,31 @@ export class NoauthBackend {
|
||||
this.updateUI()
|
||||
}
|
||||
|
||||
private async editName(npub: string, name: string) {
|
||||
const key = this.enckeys.find((k) => k.npub == npub)
|
||||
if (!key) throw new Error('Npub not found')
|
||||
if (key.name) {
|
||||
await this.sendDeleteNameToServer(npub, key.name)
|
||||
}
|
||||
if (name) {
|
||||
await this.sendNameToServer(npub, name)
|
||||
}
|
||||
await dbi.editName(npub, name)
|
||||
key.name = name
|
||||
this.updateUI()
|
||||
}
|
||||
|
||||
private async transferName(npub: string, name: string, newNpub: string) {
|
||||
const key = this.enckeys.find((k) => k.npub == npub)
|
||||
if (!key) throw new Error('Npub not found')
|
||||
if (!name) throw new Error('Empty name')
|
||||
if (key.name !== name) throw new Error('Name changed, please reload')
|
||||
await this.sendTransferNameToServer(npub, key.name, newNpub)
|
||||
await dbi.editName(npub, '')
|
||||
key.name = ''
|
||||
this.updateUI()
|
||||
}
|
||||
|
||||
private async enablePush(): Promise<boolean> {
|
||||
const options = {
|
||||
userVisibleOnly: true,
|
||||
@ -1162,6 +1222,10 @@ export class NoauthBackend {
|
||||
result = await this.deleteApp(args[0])
|
||||
} else if (method === 'deletePerm') {
|
||||
result = await this.deletePerm(args[0])
|
||||
} else if (method === 'editName') {
|
||||
result = await this.editName(args[0], args[1])
|
||||
} else if (method === 'transferName') {
|
||||
result = await this.transferName(args[0], args[1], args[2])
|
||||
} else if (method === 'enablePush') {
|
||||
result = await this.enablePush()
|
||||
} else if (method === 'fetchPendingRequests') {
|
||||
|
@ -89,6 +89,16 @@ export const dbi = {
|
||||
return []
|
||||
}
|
||||
},
|
||||
editName: async (npub: string, name: string): Promise<void> => {
|
||||
try {
|
||||
await db.keys.where({ npub }).modify({
|
||||
name,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(`db editName error: ${error}`)
|
||||
return
|
||||
}
|
||||
},
|
||||
getApp: async (appNpub: string) => {
|
||||
try {
|
||||
return await db.apps.get(appNpub)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useAppSelector } from '../../store/hooks/redux'
|
||||
import { Navigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { Stack } from '@mui/material'
|
||||
import { Box, IconButton, Stack } from '@mui/material'
|
||||
import { StyledIconButton } from './styled'
|
||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||
import { Apps } from './components/Apps'
|
||||
@ -18,7 +19,9 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { checkNpubSyncQuerier } from './utils'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded'
|
||||
import { ModalEditName } from '@/components/Modal/ModalEditName/ModalEditName'
|
||||
|
||||
const KeyPage = () => {
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
@ -60,6 +63,7 @@ const KeyPage = () => {
|
||||
|
||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -70,13 +74,20 @@ const KeyPage = () => {
|
||||
<UserValueSection
|
||||
title="Your login"
|
||||
value={username}
|
||||
copyValue={username}
|
||||
endAdornment={
|
||||
<Box display={'flex'} alignItems={'center'} gap={'0.25rem'}>
|
||||
<IconButton onClick={handleOpenEditNameModal} color={username ? 'default' : 'error'}>
|
||||
<MoreHorizRoundedIcon />
|
||||
</IconButton>
|
||||
<InputCopyButton value={username} />
|
||||
</Box>
|
||||
}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||
/>
|
||||
<UserValueSection
|
||||
title="Your NPUB"
|
||||
value={npub}
|
||||
copyValue={npub}
|
||||
endAdornment={<InputCopyButton value={npub} />}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
/>
|
||||
|
||||
@ -98,11 +109,13 @@ const KeyPage = () => {
|
||||
|
||||
<Apps apps={filteredApps} npub={npub} />
|
||||
</Stack>
|
||||
|
||||
<ModalConnectApp />
|
||||
<ModalSettings isSynced={isSynced} />
|
||||
<ModalExplanation />
|
||||
<ModalConfirmConnect />
|
||||
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
||||
<ModalEditName />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { Box, Stack } from '@mui/material'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import { StyledInput } from '../styled'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
|
||||
@ -11,10 +10,10 @@ type UserValueSectionProps = {
|
||||
title: string
|
||||
value: string
|
||||
explanationType: EXPLANATION_MODAL_KEYS
|
||||
copyValue: string
|
||||
endAdornment?: React.ReactNode
|
||||
}
|
||||
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, endAdornment }) => {
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||
@ -30,7 +29,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
|
||||
<SectionTitle>{title}</SectionTitle>
|
||||
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
||||
</Stack>
|
||||
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
|
||||
<StyledInput value={value} readOnly endAdornment={endAdornment} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -39,9 +39,9 @@ const StyledButton = styled(
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
'&.disabled': {
|
||||
'&.button.disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
background: `${theme.palette.primary.main}50`,
|
||||
background: `${theme.palette.primary.main}75`,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
|
@ -26,7 +26,12 @@ export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||
{label}
|
||||
</FormLabel>
|
||||
) : null}
|
||||
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
||||
<InputBase
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
classes={{ error: 'error', root: 'input_root', input: 'input', disabled: 'disabled' }}
|
||||
ref={ref}
|
||||
/>
|
||||
{helperText ? (
|
||||
<FormHelperText {...helperTextProps} className="helper_text">
|
||||
{helperText}
|
||||
@ -41,20 +46,20 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
width: '100%',
|
||||
'& > .input': {
|
||||
'& > .input_root': {
|
||||
background: isDark ? '#000000A8' : '#000',
|
||||
color: theme.palette.common.white,
|
||||
padding: '0.75rem 1rem',
|
||||
borderRadius: '1rem',
|
||||
border: '0.3px solid #FFFFFF54',
|
||||
fontSize: '0.875rem',
|
||||
'& input::placeholder': {
|
||||
color: '#fff',
|
||||
},
|
||||
'&.error': {
|
||||
border: '0.3px solid ' + theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
'& .input:is(.disabled, &)': {
|
||||
WebkitTextFillColor: '#ffffff80',
|
||||
},
|
||||
'& > .helper_text': {
|
||||
margin: '0.5rem 1rem 0',
|
||||
color: theme.palette.text.primary,
|
||||
|
@ -10,6 +10,7 @@ export enum MODAL_PARAMS_KEYS {
|
||||
CONFIRM_EVENT = 'confirm-event',
|
||||
ACTIVITY = 'activity',
|
||||
APP_DETAILS = 'app-details',
|
||||
EDIT_NAME = 'edit-name',
|
||||
}
|
||||
|
||||
export enum EXPLANATION_MODAL_KEYS {
|
||||
|
Reference in New Issue
Block a user