mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-06-03 21:02:05 +08:00
fix: align backup dialogs with in-app modal behavior
This commit is contained in:
parent
6f36613103
commit
15de400f2f
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ CLAUDE.md
|
||||
.vfox.toml
|
||||
.vfox/
|
||||
.claude
|
||||
AGENTS.md
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- 备份设置功能异常
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
|
||||
@ -42,7 +42,6 @@
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^9.0.0",
|
||||
"@mui/lab": "9.0.0-beta.2",
|
||||
"@mui/material": "^9.0.0",
|
||||
"@tanstack/react-query": "^5.96.1",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
|
||||
@ -32,9 +32,6 @@ importers:
|
||||
'@mui/icons-material':
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
|
||||
'@mui/lab':
|
||||
specifier: 9.0.0-beta.2
|
||||
version: 9.0.0-beta.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@mui/material':
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
@ -1068,27 +1065,6 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/lab@9.0.0-beta.2':
|
||||
resolution: {integrity: sha512-UqykXQCn7HNSExyYBvrFpaaEEmUwHbEgjFDBApEkawVPcBILyYNFhpXoqkwkafiZy+WsvcxIeRF0z4tCgisTRg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
'@emotion/styled': ^11.3.0
|
||||
'@mui/material': ^9.0.0
|
||||
'@mui/material-pigment-css': ^9.0.0
|
||||
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
'@mui/material-pigment-css':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/material@9.0.0':
|
||||
resolution: {integrity: sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -4651,22 +4627,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@mui/lab@9.0.0-beta.2(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@mui/material': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@mui/system': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
|
||||
'@mui/types': 9.0.0(@types/react@19.2.14)
|
||||
'@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
|
||||
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
|
||||
'@types/react': 19.2.14
|
||||
|
||||
'@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@ -66,9 +65,9 @@ export const BaseDialog: React.FC<Props> = ({
|
||||
</Button>
|
||||
)}
|
||||
{!disableOk && (
|
||||
<LoadingButton loading={loading} variant="contained" onClick={onOk}>
|
||||
<Button loading={loading} variant="contained" onClick={onOk}>
|
||||
{okBtn}
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
)}
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@mui/material'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
title: string
|
||||
message: string
|
||||
onClose: () => void
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export const ConfirmViewer = (props: Props) => {
|
||||
const { open, title, message, onClose, onConfirm } = props
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ pb: 1, userSelect: 'text' }}>
|
||||
{message}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
{t('shared.actions.cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} variant="contained">
|
||||
{t('shared.actions.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -22,7 +22,7 @@ import dayjs from 'dayjs'
|
||||
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
||||
import { BaseDialog } from '@/components/base'
|
||||
import { EditorViewer } from '@/components/profile/editor-viewer'
|
||||
import { GroupsEditorViewer } from '@/components/profile/groups-editor-viewer'
|
||||
import { RulesEditorViewer } from '@/components/profile/rules-editor-viewer'
|
||||
@ -990,16 +990,23 @@ export const ProfileItem = (props: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmViewer
|
||||
<BaseDialog
|
||||
title={t('profiles.modals.confirmDelete.title')}
|
||||
message={t('profiles.modals.confirmDelete.message')}
|
||||
open={confirmOpen}
|
||||
okBtn={t('shared.actions.confirm')}
|
||||
cancelBtn={t('shared.actions.cancel')}
|
||||
contentSx={{ width: { xs: 320, sm: 420 }, userSelect: 'text' }}
|
||||
onCancel={() => setConfirmOpen(false)}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
onConfirm={() => {
|
||||
onOk={() => {
|
||||
onDelete()
|
||||
setConfirmOpen(false)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Typography variant="body2" sx={{ wordBreak: 'break-word' }}>
|
||||
{t('profiles.modals.confirmDelete.message')}
|
||||
</Typography>
|
||||
</BaseDialog>
|
||||
{qrOpen && itemData.url && (
|
||||
<QrViewer
|
||||
open={true}
|
||||
|
||||
@ -49,6 +49,11 @@ const DATE_FORMAT = 'YYYY-MM-DD_HH-mm-ss'
|
||||
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/
|
||||
|
||||
type BackupSource = 'local' | 'webdav'
|
||||
type PendingConfirmation = {
|
||||
action: 'delete' | 'restore'
|
||||
filename: string
|
||||
source: BackupSource
|
||||
} | null
|
||||
|
||||
interface BackupHistoryViewerProps {
|
||||
open: boolean
|
||||
@ -67,11 +72,6 @@ interface BackupRow {
|
||||
sort_value: number
|
||||
}
|
||||
|
||||
const confirmAsync = async (message: string) => {
|
||||
const fn = window.confirm as (msg?: string) => boolean
|
||||
return fn(message)
|
||||
}
|
||||
|
||||
export const BackupHistoryViewer = ({
|
||||
open,
|
||||
source,
|
||||
@ -86,6 +86,9 @@ export const BackupHistoryViewer = ({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isRestoring, setIsRestoring] = useState(false)
|
||||
const [isRestarting, setIsRestarting] = useState(false)
|
||||
const [isConfirming, setIsConfirming] = useState(false)
|
||||
const [pendingConfirmation, setPendingConfirmation] =
|
||||
useState<PendingConfirmation>(null)
|
||||
const isLocal = source === 'local'
|
||||
const isWebDavConfigured = Boolean(
|
||||
verge?.webdav_url && verge?.webdav_username && verge?.webdav_password,
|
||||
@ -94,7 +97,7 @@ export const BackupHistoryViewer = ({
|
||||
const webdavStatus = getWebdavStatus(webdavSignature)
|
||||
const shouldSkipWebDav = !isLocal && !isWebDavConfigured
|
||||
const pageSize = 8
|
||||
const isBusy = loading || isRestoring || isRestarting
|
||||
const isBusy = loading || isRestoring || isRestarting || isConfirming
|
||||
|
||||
const buildRow = useCallback(
|
||||
(item: ILocalBackupFile | IWebDavFile): BackupRow | null => {
|
||||
@ -204,45 +207,54 @@ export const BackupHistoryViewer = ({
|
||||
})
|
||||
}, [isLocal, rows, shouldSkipWebDav, t, total, webdavStatus])
|
||||
|
||||
const handleDelete = useLockFn(async (filename: string) => {
|
||||
const handleDelete = (filename: string) => {
|
||||
if (isRestarting) return
|
||||
if (
|
||||
!(await confirmAsync(t('settings.modals.backup.messages.confirmDelete')))
|
||||
)
|
||||
return
|
||||
if (isLocal) {
|
||||
await deleteLocalBackup(filename)
|
||||
} else {
|
||||
await deleteWebdavBackup(filename)
|
||||
}
|
||||
await fetchRows()
|
||||
})
|
||||
setPendingConfirmation({ action: 'delete', filename, source })
|
||||
}
|
||||
|
||||
const handleRestore = useLockFn(async (filename: string) => {
|
||||
const handleRestore = (filename: string) => {
|
||||
if (isRestoring || isRestarting) return
|
||||
if (
|
||||
!(await confirmAsync(t('settings.modals.backup.messages.confirmRestore')))
|
||||
)
|
||||
return
|
||||
setIsRestoring(true)
|
||||
setPendingConfirmation({ action: 'restore', filename, source })
|
||||
}
|
||||
|
||||
const handleConfirmAction = useLockFn(async () => {
|
||||
if (!pendingConfirmation) return
|
||||
const { action, filename, source: actionSource } = pendingConfirmation
|
||||
const actionIsLocal = actionSource === 'local'
|
||||
setIsConfirming(true)
|
||||
if (action === 'restore') {
|
||||
setIsRestoring(true)
|
||||
}
|
||||
try {
|
||||
if (isLocal) {
|
||||
await restoreLocalBackup(filename)
|
||||
if (action === 'delete') {
|
||||
if (actionIsLocal) {
|
||||
await deleteLocalBackup(filename)
|
||||
} else {
|
||||
await deleteWebdavBackup(filename)
|
||||
}
|
||||
setPendingConfirmation(null)
|
||||
await fetchRows()
|
||||
} else {
|
||||
await restoreWebDavBackup(filename)
|
||||
if (actionIsLocal) {
|
||||
await restoreLocalBackup(filename)
|
||||
} else {
|
||||
await restoreWebDavBackup(filename)
|
||||
}
|
||||
setPendingConfirmation(null)
|
||||
showNotice.success('settings.modals.backup.messages.restoreSuccess')
|
||||
setIsRestarting(true)
|
||||
window.setTimeout(() => {
|
||||
void restartApp().catch((err: unknown) => {
|
||||
setIsRestarting(false)
|
||||
showNotice.error(err)
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
showNotice.success('settings.modals.backup.messages.restoreSuccess')
|
||||
setIsRestarting(true)
|
||||
window.setTimeout(() => {
|
||||
void restartApp().catch((err: unknown) => {
|
||||
setIsRestarting(false)
|
||||
showNotice.error(err)
|
||||
})
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
showNotice.error(error)
|
||||
} finally {
|
||||
setIsConfirming(false)
|
||||
setIsRestoring(false)
|
||||
}
|
||||
})
|
||||
@ -267,6 +279,20 @@ export const BackupHistoryViewer = ({
|
||||
void fetchRows({ force: true })
|
||||
}
|
||||
|
||||
const closeConfirmDialog = () => {
|
||||
if (isConfirming) return
|
||||
setPendingConfirmation(null)
|
||||
}
|
||||
|
||||
const confirmTitle =
|
||||
pendingConfirmation?.action === 'delete'
|
||||
? t('settings.modals.backup.actions.deleteBackup')
|
||||
: t('settings.modals.backup.actions.restoreBackup')
|
||||
const confirmMessage =
|
||||
pendingConfirmation?.action === 'delete'
|
||||
? t('settings.modals.backup.messages.confirmDelete')
|
||||
: t('settings.modals.backup.messages.confirmRestore')
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
@ -423,6 +449,30 @@ export const BackupHistoryViewer = ({
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<BaseDialog
|
||||
open={pendingConfirmation !== null}
|
||||
title={confirmTitle}
|
||||
okBtn={t('shared.actions.confirm')}
|
||||
cancelBtn={t('shared.actions.cancel')}
|
||||
contentSx={{ width: { xs: 320, sm: 420 } }}
|
||||
loading={isConfirming}
|
||||
onCancel={closeConfirmDialog}
|
||||
onClose={closeConfirmDialog}
|
||||
onOk={handleConfirmAction}
|
||||
>
|
||||
<Typography variant="body2" sx={{ wordBreak: 'break-word' }}>
|
||||
{confirmMessage}
|
||||
</Typography>
|
||||
{pendingConfirmation?.filename && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block', mt: 1, wordBreak: 'break-all' }}
|
||||
>
|
||||
{pendingConfirmation.filename}
|
||||
</Typography>
|
||||
)}
|
||||
</BaseDialog>
|
||||
</BaseDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import {
|
||||
Button,
|
||||
List,
|
||||
@ -156,7 +155,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
title: t('settings.modals.backup.tabs.local'),
|
||||
description: t('settings.modals.backup.manual.local'),
|
||||
actions: [
|
||||
<LoadingButton
|
||||
<Button
|
||||
key="backup"
|
||||
variant="contained"
|
||||
size="small"
|
||||
@ -165,7 +164,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClick={() => handleBackup('local')}
|
||||
>
|
||||
{t('settings.modals.backup.actions.backup')}
|
||||
</LoadingButton>,
|
||||
</Button>,
|
||||
<Button
|
||||
key="history"
|
||||
variant="outlined"
|
||||
@ -175,7 +174,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
>
|
||||
{t('settings.modals.backup.actions.viewHistory')}
|
||||
</Button>,
|
||||
<LoadingButton
|
||||
<Button
|
||||
key="import"
|
||||
variant="text"
|
||||
size="small"
|
||||
@ -184,7 +183,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClick={() => handleImport()}
|
||||
>
|
||||
{t('settings.modals.backup.actions.importBackup')}
|
||||
</LoadingButton>,
|
||||
</Button>,
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -192,7 +191,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
title: t('settings.modals.backup.tabs.webdav'),
|
||||
description: t('settings.modals.backup.manual.webdav'),
|
||||
actions: [
|
||||
<LoadingButton
|
||||
<Button
|
||||
key="backup"
|
||||
variant="contained"
|
||||
size="small"
|
||||
@ -200,7 +199,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClick={() => handleBackup('webdav')}
|
||||
>
|
||||
{t('settings.modals.backup.actions.backup')}
|
||||
</LoadingButton>,
|
||||
</Button>,
|
||||
<Button
|
||||
key="history"
|
||||
variant="outlined"
|
||||
|
||||
@ -2,9 +2,9 @@ import {
|
||||
RestartAltRounded,
|
||||
SwitchAccessShortcutRounded,
|
||||
} from '@mui/icons-material'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
List,
|
||||
@ -120,7 +120,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
{t('settings.sections.clash.form.fields.clashCore')}
|
||||
<Box>
|
||||
<LoadingButton
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<SwitchAccessShortcutRounded />}
|
||||
@ -131,8 +131,8 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClick={onUpgrade}
|
||||
>
|
||||
{t('shared.actions.upgrade')}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<RestartAltRounded />}
|
||||
@ -142,7 +142,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
onClick={onRestart}
|
||||
>
|
||||
{t('shared.actions.restart')}
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ import {
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
} from '@mui/icons-material'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import { Box, Button, Divider, Grid, IconButton, Stack } from '@mui/material'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { listen, TauriEvent } from '@tauri-apps/api/event'
|
||||
@ -970,7 +969,7 @@ const ProfilePage = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<LoadingButton
|
||||
<Button
|
||||
disabled={!url || disabled}
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
@ -979,7 +978,7 @@ const ProfilePage = () => {
|
||||
onClick={onImport}
|
||||
>
|
||||
{t('profiles.page.actions.import')}
|
||||
</LoadingButton>
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user