Add sort options to cmd-k menu
This commit is contained in:
parent
c2b1be5fb4
commit
c78c6fd7e0
@ -3,7 +3,6 @@ import SwitcherItem from '@/components/switcher/SwitcherItem';
|
||||
import IconFull from '@/components/icons/IconFull';
|
||||
import IconGrid from '@/components/icons/IconGrid';
|
||||
import {
|
||||
doesPathOfferSort,
|
||||
PATH_FULL_INFERRED,
|
||||
PATH_GRID_INFERRED,
|
||||
} from '@/app/path';
|
||||
@ -23,7 +22,7 @@ import { usePathname } from 'next/navigation';
|
||||
import { KEY_COMMANDS } from '@/photo/key-commands';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import IconSort from '@/components/icons/IconSort';
|
||||
import { getSortConfigFromPath } from '@/photo/sort/path';
|
||||
import { getSortStateFromPath } from '@/photo/sort/path';
|
||||
import { motion } from 'framer-motion';
|
||||
import SortMenu from '@/photo/sort/SortMenu';
|
||||
import { SWR_KEYS } from '@/swr';
|
||||
@ -53,10 +52,11 @@ export default function AppViewSwitcher({
|
||||
invalidateSwr,
|
||||
} = useAppState();
|
||||
|
||||
const sortConfig = useMemo(() => getSortConfigFromPath(pathname), [pathname]);
|
||||
const sortConfig = useMemo(() => getSortStateFromPath(pathname), [pathname]);
|
||||
|
||||
const {
|
||||
sortBy,
|
||||
doesPathOfferSort,
|
||||
isSortedByDefault,
|
||||
isAscending,
|
||||
pathGrid,
|
||||
@ -66,7 +66,7 @@ export default function AppViewSwitcher({
|
||||
|
||||
const showSortControl =
|
||||
NAV_SORT_CONTROL !== 'none' &&
|
||||
doesPathOfferSort(pathname);
|
||||
doesPathOfferSort;
|
||||
|
||||
const hasLoadedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
@ -194,7 +194,7 @@ export default function AppViewSwitcher({
|
||||
/>}
|
||||
tooltip={{
|
||||
...!isSortMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||
content: 'Sort',
|
||||
content: appText.sort.sort,
|
||||
},
|
||||
}}
|
||||
width="narrow"
|
||||
@ -210,11 +210,11 @@ export default function AppViewSwitcher({
|
||||
sort={isAscending ? 'asc' : 'desc'}
|
||||
className="translate-x-[0.5px] translate-y-[1px]"
|
||||
/>}
|
||||
tooltip={{
|
||||
tooltip={{...SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
|
||||
content: isAscending
|
||||
? appText.sort.newest
|
||||
: appText.sort.oldest,
|
||||
}}
|
||||
? appText.sort.viewNewest
|
||||
: appText.sort.viewOldest,
|
||||
}}}
|
||||
width="narrow"
|
||||
noPadding
|
||||
/>}
|
||||
|
||||
@ -40,7 +40,7 @@ import Spinner from '../components/Spinner';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { BiDesktop, BiLockAlt, BiMoon, BiSun } from 'react-icons/bi';
|
||||
import { IoInvertModeSharp } from 'react-icons/io5';
|
||||
import { IoClose, IoInvertModeSharp } from 'react-icons/io5';
|
||||
import { useAppState } from '@/app/AppState';
|
||||
import { searchPhotosAction } from '@/photo/actions';
|
||||
import { RiToolsFill } from 'react-icons/ri';
|
||||
@ -86,10 +86,12 @@ import IconFavs from '@/components/icons/IconFavs';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
||||
import IconRecents from '@/components/icons/IconRecents';
|
||||
import { CgFileDocument } from 'react-icons/cg';
|
||||
import { CgClose, CgFileDocument } from 'react-icons/cg';
|
||||
import { FaRegUserCircle } from 'react-icons/fa';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import IconCheck from '@/components/icons/IconCheck';
|
||||
import { getSortStateFromPath } from '@/photo/sort/path';
|
||||
import IconSort from '@/components/icons/IconSort';
|
||||
|
||||
const DIALOG_TITLE = 'Global Command-K Menu';
|
||||
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
|
||||
@ -116,6 +118,11 @@ type CommandKSection = {
|
||||
items: CommandKItem[]
|
||||
}
|
||||
|
||||
const renderCheck = (isChecked?: boolean) =>
|
||||
isChecked
|
||||
? <IconCheck size={12} className="translate-y-[-0.5px]" />
|
||||
: undefined;
|
||||
|
||||
const renderToggle = (
|
||||
label: string,
|
||||
onToggle?: Dispatch<SetStateAction<boolean>>,
|
||||
@ -123,7 +130,7 @@ const renderToggle = (
|
||||
): CommandKItem => ({
|
||||
label: `Toggle ${label}`,
|
||||
action: () => onToggle?.(prev => !prev),
|
||||
annotation: isEnabled ? <IconCheck size={12} /> : undefined,
|
||||
annotation: renderCheck(isEnabled),
|
||||
});
|
||||
|
||||
export default function CommandKClient({
|
||||
@ -172,6 +179,19 @@ export default function CommandKClient({
|
||||
setShouldDebugRecipeOverlays,
|
||||
} = useAppState();
|
||||
|
||||
const {
|
||||
doesPathOfferSort,
|
||||
isSortedByDefault,
|
||||
pathNewest,
|
||||
pathOldest,
|
||||
pathTakenAt,
|
||||
pathUploadedAt,
|
||||
pathClearSort,
|
||||
isAscending,
|
||||
isTakenAt,
|
||||
isUploadedAt,
|
||||
} = useMemo(() => getSortStateFromPath(pathname), [pathname]);
|
||||
|
||||
const appText = useAppText();
|
||||
|
||||
const isOpenRef = useRef(isOpen);
|
||||
@ -209,7 +229,7 @@ export default function CommandKClient({
|
||||
}, [isWaiting, setIsOpen]);
|
||||
|
||||
// Raw query values
|
||||
const [queryLiveRaw, setQueryLive] = useState('');
|
||||
const [queryLiveRaw, setQueryLiveRaw] = useState('');
|
||||
const [queryDebouncedRaw] =
|
||||
useDebounce(queryLiveRaw, 500, { trailing: true });
|
||||
|
||||
@ -289,7 +309,7 @@ export default function CommandKClient({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setQueryLive('');
|
||||
setQueryLiveRaw('');
|
||||
setQueriedSections([]);
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -305,6 +325,7 @@ export default function CommandKClient({
|
||||
return count ? { count, subhead } : undefined;
|
||||
}, [recent, appText]);
|
||||
|
||||
// Years only accessible by search
|
||||
const years = useMemo(() =>
|
||||
_years.filter(({ year }) => queryLive && year.includes(queryLive))
|
||||
, [_years, queryLive]);
|
||||
@ -503,6 +524,40 @@ export default function CommandKClient({
|
||||
});
|
||||
}
|
||||
|
||||
const sortItems = [{
|
||||
label: appText.sort.newestFirst,
|
||||
path: pathNewest,
|
||||
annotation: renderCheck(!isAscending),
|
||||
}, {
|
||||
label: appText.sort.oldestFirst,
|
||||
path: pathOldest,
|
||||
annotation: renderCheck(isAscending),
|
||||
}, {
|
||||
label: appText.sort.byTakenAt,
|
||||
path: pathTakenAt,
|
||||
annotation: renderCheck(isTakenAt),
|
||||
}, {
|
||||
label: appText.sort.byUploadedAt,
|
||||
path: pathUploadedAt,
|
||||
annotation: renderCheck(isUploadedAt),
|
||||
}];
|
||||
|
||||
if (!isSortedByDefault) {
|
||||
sortItems.push({
|
||||
label: appText.sort.clearSort,
|
||||
path: pathClearSort,
|
||||
annotation: <CgClose />,
|
||||
});
|
||||
}
|
||||
|
||||
const sortSection: CommandKSection = {
|
||||
heading: appText.sort.sort,
|
||||
accessory: <IconSort size={14} className="translate-x-[0.5px]" />,
|
||||
items: doesPathOfferSort
|
||||
? sortItems
|
||||
: [],
|
||||
};
|
||||
|
||||
const pageFull: CommandKItem = {
|
||||
label: GRID_HOMEPAGE_ENABLED
|
||||
? appText.nav.full
|
||||
@ -522,13 +577,13 @@ export default function CommandKClient({
|
||||
: [pageFull, pageGrid];
|
||||
|
||||
const sectionPages: CommandKSection = {
|
||||
heading: 'Pages',
|
||||
heading: appText.cmdk.pages,
|
||||
accessory: <CgFileDocument size={14} className="translate-x-[-0.5px]" />,
|
||||
items: pageItems,
|
||||
};
|
||||
|
||||
const adminSection: CommandKSection = {
|
||||
heading: 'Admin',
|
||||
heading: appText.nav.admin,
|
||||
accessory: <FaRegUserCircle
|
||||
size={13}
|
||||
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
||||
@ -649,8 +704,9 @@ export default function CommandKClient({
|
||||
)}>
|
||||
<Command.Input
|
||||
ref={refInput}
|
||||
onChangeCapture={(e) => {
|
||||
setQueryLive(e.currentTarget.value);
|
||||
value={queryLiveRaw}
|
||||
onValueChange={value => {
|
||||
setQueryLiveRaw(value);
|
||||
updateMask();
|
||||
}}
|
||||
className={clsx(
|
||||
@ -667,20 +723,30 @@ export default function CommandKClient({
|
||||
disabled={isPending}
|
||||
/>
|
||||
{isLoading && !isPending
|
||||
? <span className="mr-1 translate-y-[2px]">
|
||||
<Spinner size={16} />
|
||||
? <span className="translate-y-[2px]">
|
||||
<Spinner size={16} className="-mr-1" />
|
||||
</span>
|
||||
: <span className="max-sm:hidden">
|
||||
<LoaderButton
|
||||
className={clsx(
|
||||
'h-auto! px-1.5 py-1 -mr-1.5',
|
||||
'h-auto! py-1 -mr-2',
|
||||
'border-medium shadow-none',
|
||||
queryLiveRaw ? 'px-1' : 'px-1.5',
|
||||
'text-[12px]',
|
||||
'text-gray-400/90 dark:text-gray-700',
|
||||
)}
|
||||
onClick={() => setIsOpen?.(false)}
|
||||
onClick={() => {
|
||||
if (queryLiveRaw) {
|
||||
setQueryLiveRaw('');
|
||||
updateMask();
|
||||
} else {
|
||||
setIsOpen?.(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
ESC
|
||||
{queryLiveRaw
|
||||
? <IoClose size={17} className="text-dim" />
|
||||
: 'ESC'}
|
||||
</LoaderButton>
|
||||
</span>}
|
||||
</div>
|
||||
@ -698,6 +764,7 @@ export default function CommandKClient({
|
||||
</Command.Empty>
|
||||
{queriedSections
|
||||
.concat(categorySections)
|
||||
.concat(sortSection)
|
||||
.concat(sectionPages)
|
||||
.concat(adminSection)
|
||||
.concat(clientSections)
|
||||
|
||||
@ -55,13 +55,25 @@ export const TEXT: I18N = {
|
||||
nextShort: 'পরবর্তী',
|
||||
},
|
||||
sort: {
|
||||
newest: 'নতুনতম দেখুন',
|
||||
oldest: 'পুরাতনতম দেখুন',
|
||||
sort: 'সাজান',
|
||||
newest: 'নতুনতম',
|
||||
oldest: 'পুরাতনতম',
|
||||
newestFirst: 'নতুনতম প্রথমে',
|
||||
oldestFirst: 'পুরাতনতম প্রথমে',
|
||||
viewNewest: 'নতুনতম দেখুন',
|
||||
viewOldest: 'পুরাতনতম দেখুন',
|
||||
takenAt: 'তোলা হয়েছে',
|
||||
byTakenAt: 'তোলার সময় অনুযায়ী',
|
||||
uploadedAt: 'আপলোড হয়েছে',
|
||||
byUploadedAt: 'আপলোডের সময় অনুযায়ী',
|
||||
uploadedAtShort: 'আপলোড',
|
||||
clearSort: 'সাজানো মুছুন',
|
||||
},
|
||||
cmdk: {
|
||||
placeholder: 'ছবি, ভিউ, সেটিংস অনুসন্ধান করুন ...',
|
||||
searching: 'অনুসন্ধান হচ্ছে ...',
|
||||
noResults: 'কোনো ফলাফল পাওয়া যায়নি',
|
||||
pages: 'পৃষ্ঠাসমূহ',
|
||||
},
|
||||
tooltip: {
|
||||
'35mm': '৩৫মিমি সমতুল্য',
|
||||
|
||||
@ -53,13 +53,25 @@ export const TEXT = {
|
||||
nextShort: 'Next',
|
||||
},
|
||||
sort: {
|
||||
newest: 'View newest',
|
||||
oldest: 'View oldest',
|
||||
sort: 'Sort',
|
||||
newest: 'Newest',
|
||||
oldest: 'Oldest',
|
||||
newestFirst: 'Newest first',
|
||||
oldestFirst: 'Oldest first',
|
||||
viewNewest: 'View newest',
|
||||
viewOldest: 'View oldest',
|
||||
takenAt: 'Taken at',
|
||||
byTakenAt: 'By taken at',
|
||||
uploadedAt: 'Uploaded at',
|
||||
byUploadedAt: 'By uploaded at',
|
||||
uploadedAtShort: 'Uploaded',
|
||||
clearSort: 'Clear sort',
|
||||
},
|
||||
cmdk: {
|
||||
placeholder: 'Search photos, views, settings ...',
|
||||
searching: 'Searching ...',
|
||||
noResults: 'No results found',
|
||||
pages: 'Pages',
|
||||
},
|
||||
tooltip: {
|
||||
'35mm': '35mm Equivalent',
|
||||
|
||||
@ -54,13 +54,25 @@ export const TEXT: I18N = {
|
||||
nextShort: 'Brkt',
|
||||
},
|
||||
sort: {
|
||||
newest: 'Lihat terbaru',
|
||||
oldest: 'Lihat terlama',
|
||||
sort: 'Urutkan',
|
||||
newest: 'Terbaru',
|
||||
oldest: 'Terlama',
|
||||
newestFirst: 'Terbaru dulu',
|
||||
oldestFirst: 'Terlama dulu',
|
||||
viewNewest: 'Lihat terbaru',
|
||||
viewOldest: 'Lihat terlama',
|
||||
takenAt: 'Diambil pada',
|
||||
byTakenAt: 'Berdasarkan waktu pengambilan',
|
||||
uploadedAt: 'Diunggah pada',
|
||||
byUploadedAt: 'Berdasarkan waktu unggahan',
|
||||
uploadedAtShort: 'Diunggah',
|
||||
clearSort: 'Hapus pengurutan',
|
||||
},
|
||||
cmdk: {
|
||||
placeholder: 'Cari foto, tampilan, pengaturan ...',
|
||||
searching: 'Mencari ...',
|
||||
noResults: 'Tidak ada hasil ditemukan',
|
||||
pages: 'Halaman',
|
||||
},
|
||||
tooltip: {
|
||||
'35mm': 'Setara 35mm',
|
||||
|
||||
@ -54,13 +54,25 @@ export const TEXT: I18N = {
|
||||
nextShort: 'Próx',
|
||||
},
|
||||
sort: {
|
||||
newest: 'Ver mais recentes',
|
||||
oldest: 'Ver mais antigas',
|
||||
sort: 'Ordenar',
|
||||
newest: 'Mais recentes',
|
||||
oldest: 'Mais antigas',
|
||||
newestFirst: 'Mais recentes primeiro',
|
||||
oldestFirst: 'Mais antigas primeiro',
|
||||
viewNewest: 'Ver mais recentes',
|
||||
viewOldest: 'Ver mais antigas',
|
||||
takenAt: 'Tirado em',
|
||||
byTakenAt: 'Por data de captura',
|
||||
uploadedAt: 'Enviado em',
|
||||
byUploadedAt: 'Por data de envio',
|
||||
uploadedAtShort: 'Enviado',
|
||||
clearSort: 'Limpar ordenação',
|
||||
},
|
||||
cmdk: {
|
||||
placeholder: 'Pesquisar fotos, visualizações, configurações ...',
|
||||
searching: 'Pesquisando ...',
|
||||
noResults: 'Nenhum resultado encontrado',
|
||||
pages: 'Páginas',
|
||||
},
|
||||
tooltip: {
|
||||
'35mm': 'Equivalente em 35mm',
|
||||
|
||||
@ -54,13 +54,25 @@ export const TEXT: I18N = {
|
||||
nextShort: 'Próx',
|
||||
},
|
||||
sort: {
|
||||
newest: 'Ver mais recentes',
|
||||
oldest: 'Ver mais antigas',
|
||||
sort: 'Ordenar',
|
||||
newest: 'Mais recentes',
|
||||
oldest: 'Mais antigas',
|
||||
newestFirst: 'Mais recentes primeiro',
|
||||
oldestFirst: 'Mais antigas primeiro',
|
||||
viewNewest: 'Ver mais recentes',
|
||||
viewOldest: 'Ver mais antigas',
|
||||
takenAt: 'Tirado em',
|
||||
byTakenAt: 'Por data de captura',
|
||||
uploadedAt: 'Enviado em',
|
||||
byUploadedAt: 'Por data de envio',
|
||||
uploadedAtShort: 'Enviado',
|
||||
clearSort: 'Limpar ordenação',
|
||||
},
|
||||
cmdk: {
|
||||
placeholder: 'Pesquisar fotografias, visualizações, configurações ...',
|
||||
searching: 'A pesquisar ...',
|
||||
noResults: 'Nenhum resultado encontrado',
|
||||
pages: 'Páginas',
|
||||
},
|
||||
tooltip: {
|
||||
'35mm': 'Equivalente em 35mm',
|
||||
|
||||
@ -54,13 +54,25 @@ export const TEXT: I18N = {
|
||||
nextShort: '下一页',
|
||||
},
|
||||
sort: {
|
||||
newest: '查看最新',
|
||||
oldest: '查看最旧',
|
||||
sort: '排序',
|
||||
newest: '最新',
|
||||
oldest: '最旧',
|
||||
newestFirst: '最新优先',
|
||||
oldestFirst: '最旧优先',
|
||||
viewNewest: '查看最新',
|
||||
viewOldest: '查看最旧',
|
||||
takenAt: '拍摄时间',
|
||||
byTakenAt: '按拍摄时间',
|
||||
uploadedAt: '上传时间',
|
||||
byUploadedAt: '按上传时间',
|
||||
uploadedAtShort: '上传',
|
||||
clearSort: '清除排序',
|
||||
},
|
||||
cmdk: {
|
||||
placeholder: '搜索照片、视图、设置...',
|
||||
searching: '搜索中...',
|
||||
noResults: '未找到结果',
|
||||
pages: '页面',
|
||||
},
|
||||
tooltip: {
|
||||
'35mm': '35mm 等效焦距',
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import IconSort from '@/components/icons/IconSort';
|
||||
import SwitcherItemMenu from '@/components/switcher/SwitcherItemMenu';
|
||||
import { getSortConfigFromPath } from './path';
|
||||
import { getSortStateFromPath } from './path';
|
||||
import IconCheck from '@/components/icons/IconCheck';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useAppText } from '@/i18n/state/client';
|
||||
|
||||
export default function SortMenu({
|
||||
isOpen,
|
||||
@ -17,7 +18,9 @@ export default function SortMenu({
|
||||
}: {
|
||||
isOpen?: boolean
|
||||
setIsOpen?: (isOpen: boolean) => void
|
||||
} & ReturnType<typeof getSortConfigFromPath>) {
|
||||
} & ReturnType<typeof getSortStateFromPath>) {
|
||||
const appText = useAppText();
|
||||
|
||||
const renderIcon = (isChecked: boolean) => isChecked
|
||||
? <IconCheck size={13} className="translate-x-[-2px]" />
|
||||
: <span />;
|
||||
@ -38,21 +41,21 @@ export default function SortMenu({
|
||||
/>}
|
||||
sections={[{
|
||||
items: [{
|
||||
...renderLabel('Newest', !isAscending),
|
||||
...renderLabel(appText.sort.newest, !isAscending),
|
||||
icon: renderIcon(!isAscending),
|
||||
href: pathNewest,
|
||||
}, {
|
||||
...renderLabel('Oldest', isAscending),
|
||||
...renderLabel(appText.sort.oldest, isAscending),
|
||||
icon: renderIcon(isAscending),
|
||||
href: pathOldest,
|
||||
}],
|
||||
}, {
|
||||
items: [{
|
||||
...renderLabel('Taken at', isTakenAt),
|
||||
...renderLabel(appText.sort.takenAt, isTakenAt),
|
||||
icon: renderIcon(isTakenAt),
|
||||
href: pathTakenAt,
|
||||
}, {
|
||||
...renderLabel('Uploaded', isUploadedAt),
|
||||
...renderLabel(appText.sort.uploadedAtShort, isUploadedAt),
|
||||
icon: renderIcon(isUploadedAt),
|
||||
href: pathUploadedAt,
|
||||
}],
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// to avoid circular dependencies
|
||||
|
||||
import {
|
||||
doesPathOfferSort as _doesPathOfferSort,
|
||||
PARAM_SORT_ORDER_NEWEST,
|
||||
PARAM_SORT_ORDER_OLDEST,
|
||||
PARAM_SORT_TYPE_TAKEN_AT,
|
||||
@ -16,7 +17,7 @@ import {
|
||||
USER_DEFAULT_SORT_WITH_PRIORITY,
|
||||
} from '@/app/config';
|
||||
|
||||
export const getSortByComponents = (sortBy: SortBy): {
|
||||
const getSortByComponents = (sortBy: SortBy): {
|
||||
sortType: string
|
||||
sortOrder: string
|
||||
} => {
|
||||
@ -40,7 +41,7 @@ export const getSortByComponents = (sortBy: SortBy): {
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
export const {
|
||||
sortType: DEFAULT_SORT_TYPE,
|
||||
sortOrder: DEFAULT_SORT_ORDER,
|
||||
} = getSortByComponents(USER_DEFAULT_SORT_BY);
|
||||
@ -95,7 +96,9 @@ const getPathSortComponents = (pathname: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getSortConfigFromPath = (pathname: string) => {
|
||||
export const getSortStateFromPath = (pathname: string) => {
|
||||
const doesPathOfferSort = _doesPathOfferSort(pathname);
|
||||
|
||||
const {
|
||||
gridOrFull: _gridOrFull,
|
||||
sortType,
|
||||
@ -153,8 +156,14 @@ export const getSortConfigFromPath = (pathname: string) => {
|
||||
const pathUploadedAt =
|
||||
getPath({ sortType: PARAM_SORT_TYPE_UPLOADED_AT, sortOrder });
|
||||
|
||||
// Sort clear
|
||||
const pathClearSort = _gridOrFull === 'grid'
|
||||
? PATH_GRID_INFERRED
|
||||
: PATH_FULL_INFERRED;
|
||||
|
||||
return {
|
||||
sortBy,
|
||||
doesPathOfferSort,
|
||||
isSortedByDefault,
|
||||
isAscending,
|
||||
isTakenAt,
|
||||
@ -165,6 +174,7 @@ export const getSortConfigFromPath = (pathname: string) => {
|
||||
pathOldest,
|
||||
pathTakenAt,
|
||||
pathUploadedAt,
|
||||
pathClearSort,
|
||||
pathSortToggle,
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user