Compare commits

...

18 Commits

Author SHA1 Message Date
artur
ba3775e6c6 Make username settings button red if name empty, add sections to username settings, UI fixes to username settings, remove qs params from username settings 2024-02-19 10:37:06 +03:00
Bekbolsun
4ad66c8711 add transfer name field 2024-02-16 19:47:25 +06:00
Bekbolsun
6a04c3ec4b Merge branch 'develop' of https://github.com/nostrband/noauth into feature/edit-name 2024-02-16 18:14:59 +06:00
Bekbolsun
6d72cf1f82 implement edit username logic in edit modal 2024-02-16 17:59:59 +06:00
Nostr.Band
2e522b79ad Merge pull request #84 from nostrband/main
Merge w/ main
2024-02-16 14:48:48 +03:00
Nostr.Band
453a16690f Merge pull request #83 from nostrband/feature/ignore
Add ignore logic to stop interfering with replies from other instances
2024-02-16 14:48:16 +03:00
Nostr.Band
8ef8157c38 Merge pull request #81 from nostrband/feature/watcher
Feature/watcher
2024-02-16 13:34:29 +03:00
Nostr.Band
4f00a014d0 Merge pull request #80 from nostrband/feature/watcher
Feature/watcher
2024-02-16 13:33:50 +03:00
Nostr.Band
04373e7991 Merge pull request #79 from nostrband/develop
Show app npubs
2024-02-16 12:02:24 +03:00
Bekbolsun
d199dcf9f7 Merge branch 'feature/edit-name' of https://github.com/nostrband/noauth into feature/edit-name 2024-02-16 14:22:30 +06:00
artur
0f28c80a15 Add editName and transferName to backend 2024-02-16 09:47:46 +03:00
Nostr.Band
34b516a1e3 Merge pull request #71 from nostrband/develop
Many minor fixes in UI, spinners etc.
2024-02-15 09:28:45 +03:00
Nostr.Band
40f4a9922a Merge pull request #69 from nostrband/develop
Fix redirect to confirm connect w/ popup=true after login
2024-02-15 09:00:24 +03:00
Nostr.Band
4b1f7564e7 Merge pull request #68 from nostrband/develop
Add logic to confirm after login
2024-02-15 08:42:14 +03:00
Nostr.Band
83d5c013cf Merge pull request #65 from nostrband/develop
Show kind in sign-event in activity history, show import key without …
2024-02-14 11:40:45 +03:00
Nostr.Band
e96edf90fe Merge pull request #64 from nostrband/develop
Fix - close confirm event popup after confirmed
2024-02-14 10:51:12 +03:00
Nostr.Band
56e71219a5 Merge pull request #63 from nostrband/develop
Readme
2024-02-14 10:17:22 +03:00
Nostr.Band
67b6a3bfcf Merge pull request #62 from nostrband/develop
Develop
2024-02-14 09:58:06 +03:00
10 changed files with 294 additions and 18 deletions

View 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>
)
}

View 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,
}))

View File

@@ -100,7 +100,7 @@ export const ModalLogin = () => {
if (isPopup && isModalOpened) { if (isPopup && isModalOpened) {
swicCall('fetchPendingRequests', npub, appNpub) swicCall('fetchPendingRequests', npub, appNpub)
fetchNpubNames(npub).then(names => { fetchNpubNames(npub).then((names) => {
if (names.length) { if (names.length) {
setValue('username', `${names[0]}@${DOMAIN}`) setValue('username', `${names[0]}@${DOMAIN}`)
} }

View File

@@ -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 { // class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
// readonly backend: NDKNip46Backend // readonly backend: NDKNip46Backend
// readonly method: string // readonly method: string
@@ -508,6 +508,41 @@ export class NoauthBackend {
throw new Error('Too many requests, retry later') 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) { private async sendTokenToServer(npub: string, token: string) {
const body = JSON.stringify({ const body = JSON.stringify({
npub, npub,
@@ -766,7 +801,7 @@ export class NoauthBackend {
return // noop return // noop
case DECISION.ALLOW: case DECISION.ALLOW:
case DECISION.DISALLOW: case DECISION.DISALLOW:
// fall through // fall through
} }
const allow = decision === DECISION.ALLOW const allow = decision === DECISION.ALLOW
@@ -1116,6 +1151,31 @@ export class NoauthBackend {
this.updateUI() 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> { private async enablePush(): Promise<boolean> {
const options = { const options = {
userVisibleOnly: true, userVisibleOnly: true,
@@ -1162,6 +1222,10 @@ export class NoauthBackend {
result = await this.deleteApp(args[0]) result = await this.deleteApp(args[0])
} else if (method === 'deletePerm') { } else if (method === 'deletePerm') {
result = await this.deletePerm(args[0]) 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') { } else if (method === 'enablePush') {
result = await this.enablePush() result = await this.enablePush()
} else if (method === 'fetchPendingRequests') { } else if (method === 'fetchPendingRequests') {

View File

@@ -89,6 +89,16 @@ export const dbi = {
return [] 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) => { getApp: async (appNpub: string) => {
try { try {
return await db.apps.get(appNpub) return await db.apps.get(appNpub)

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react'
import { useAppSelector } from '../../store/hooks/redux' import { useAppSelector } from '../../store/hooks/redux'
import { Navigate, useParams, useSearchParams } from 'react-router-dom' 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 { StyledIconButton } from './styled'
import { SettingsIcon, ShareIcon } from '@/assets' import { SettingsIcon, ShareIcon } from '@/assets'
import { Apps } from './components/Apps' import { Apps } from './components/Apps'
@@ -18,7 +19,9 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils' import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts' 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 KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
@@ -60,6 +63,7 @@ const KeyPage = () => {
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS) const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
return ( return (
<> <>
@@ -70,13 +74,20 @@ const KeyPage = () => {
<UserValueSection <UserValueSection
title="Your login" title="Your login"
value={username} 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} explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
/> />
<UserValueSection <UserValueSection
title="Your NPUB" title="Your NPUB"
value={npub} value={npub}
copyValue={npub} endAdornment={<InputCopyButton value={npub} />}
explanationType={EXPLANATION_MODAL_KEYS.NPUB} explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/> />
@@ -98,11 +109,13 @@ const KeyPage = () => {
<Apps apps={filteredApps} npub={npub} /> <Apps apps={filteredApps} npub={npub} />
</Stack> </Stack>
<ModalConnectApp /> <ModalConnectApp />
<ModalSettings isSynced={isSynced} /> <ModalSettings isSynced={isSynced} />
<ModalExplanation /> <ModalExplanation />
<ModalConfirmConnect /> <ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} /> <ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
<ModalEditName />
</> </>
) )
} }

View File

@@ -3,7 +3,6 @@ import { Box, Stack } from '@mui/material'
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal' import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { AppLink } from '@/shared/AppLink/AppLink' import { AppLink } from '@/shared/AppLink/AppLink'
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
import { StyledInput } from '../styled' import { StyledInput } from '../styled'
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
@@ -11,10 +10,10 @@ type UserValueSectionProps = {
title: string title: string
value: string value: string
explanationType: EXPLANATION_MODAL_KEYS 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 { handleOpen } = useModalSearchParams()
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => { const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
@@ -30,7 +29,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
<SectionTitle>{title}</SectionTitle> <SectionTitle>{title}</SectionTitle>
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} /> <AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
</Stack> </Stack>
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} /> <StyledInput value={value} readOnly endAdornment={endAdornment} />
</Box> </Box>
) )
} }

View File

@@ -39,9 +39,9 @@ const StyledButton = styled(
background: theme.palette.primary.main, background: theme.palette.primary.main,
}, },
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
'&.disabled': { '&.button.disabled': {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
background: `${theme.palette.primary.main}50`, background: `${theme.palette.primary.main}75`,
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
} }

View File

@@ -26,7 +26,12 @@ export const Input = forwardRef<HTMLInputElement, AppInputProps>(
{label} {label}
</FormLabel> </FormLabel>
) : null} ) : 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 ? ( {helperText ? (
<FormHelperText {...helperTextProps} className="helper_text"> <FormHelperText {...helperTextProps} className="helper_text">
{helperText} {helperText}
@@ -41,20 +46,20 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({
const isDark = theme.palette.mode === 'dark' const isDark = theme.palette.mode === 'dark'
return { return {
width: '100%', width: '100%',
'& > .input': { '& > .input_root': {
background: isDark ? '#000000A8' : '#000', background: isDark ? '#000000A8' : '#000',
color: theme.palette.common.white, color: theme.palette.common.white,
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
borderRadius: '1rem', borderRadius: '1rem',
border: '0.3px solid #FFFFFF54', border: '0.3px solid #FFFFFF54',
fontSize: '0.875rem', fontSize: '0.875rem',
'& input::placeholder': {
color: '#fff',
},
'&.error': { '&.error': {
border: '0.3px solid ' + theme.palette.error.main, border: '0.3px solid ' + theme.palette.error.main,
}, },
}, },
'& .input:is(.disabled, &)': {
WebkitTextFillColor: '#ffffff80',
},
'& > .helper_text': { '& > .helper_text': {
margin: '0.5rem 1rem 0', margin: '0.5rem 1rem 0',
color: theme.palette.text.primary, color: theme.palette.text.primary,

View File

@@ -10,6 +10,7 @@ export enum MODAL_PARAMS_KEYS {
CONFIRM_EVENT = 'confirm-event', CONFIRM_EVENT = 'confirm-event',
ACTIVITY = 'activity', ACTIVITY = 'activity',
APP_DETAILS = 'app-details', APP_DETAILS = 'app-details',
EDIT_NAME = 'edit-name',
} }
export enum EXPLANATION_MODAL_KEYS { export enum EXPLANATION_MODAL_KEYS {