feat: add commands (#4477)
This commit is contained in:
@@ -100,3 +100,5 @@ export const setPageModeAtom = atom(
|
|||||||
|
|
||||||
export type PageModeOption = 'all' | 'page' | 'edgeless';
|
export type PageModeOption = 'all' | 'page' | 'edgeless';
|
||||||
export const allPageModeSelectAtom = atom<PageModeOption>('all');
|
export const allPageModeSelectAtom = atom<PageModeOption>('all');
|
||||||
|
|
||||||
|
export const openWorkspaceListModalAtom = atom(false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { PlusIcon } from '@blocksuite/icons';
|
import { ImportIcon, PlusIcon } from '@blocksuite/icons';
|
||||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||||
import type { createStore } from 'jotai';
|
import type { createStore } from 'jotai';
|
||||||
|
|
||||||
@@ -57,6 +57,20 @@ export function registerAffineCreationCommands({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:import-workspace',
|
||||||
|
category: 'affine:creation',
|
||||||
|
icon: <ImportIcon />,
|
||||||
|
label: t['com.affine.cmdk.affine.import-workspace'],
|
||||||
|
preconditionStrategy: () => {
|
||||||
|
return environment.isDesktop;
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
store.set(openCreateWorkspaceModalAtom, 'add');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubs.forEach(unsub => unsub());
|
unsubs.forEach(unsub => unsub());
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import type { Workspace } from '@blocksuite/store';
|
|||||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||||
import type { createStore } from 'jotai';
|
import type { createStore } from 'jotai';
|
||||||
|
|
||||||
import { openSettingModalAtom } from '../atoms';
|
import {
|
||||||
|
openSettingModalAtom,
|
||||||
|
openWorkspaceListModalAtom,
|
||||||
|
type PageModeOption,
|
||||||
|
} from '../atoms';
|
||||||
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|
||||||
@@ -13,10 +17,14 @@ export function registerAffineNavigationCommands({
|
|||||||
store,
|
store,
|
||||||
workspace,
|
workspace,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
|
pageMode,
|
||||||
|
setPageMode,
|
||||||
}: {
|
}: {
|
||||||
t: ReturnType<typeof useAFFiNEI18N>;
|
t: ReturnType<typeof useAFFiNEI18N>;
|
||||||
store: ReturnType<typeof createStore>;
|
store: ReturnType<typeof createStore>;
|
||||||
navigationHelper: ReturnType<typeof useNavigateHelper>;
|
navigationHelper: ReturnType<typeof useNavigateHelper>;
|
||||||
|
pageMode: PageModeOption;
|
||||||
|
setPageMode: React.Dispatch<React.SetStateAction<PageModeOption>>;
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}) {
|
}) {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
@@ -28,6 +36,51 @@ export function registerAffineNavigationCommands({
|
|||||||
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
|
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
|
||||||
run() {
|
run() {
|
||||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||||
|
setPageMode('all');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:goto-page-list',
|
||||||
|
category: 'affine:navigation',
|
||||||
|
icon: <ArrowRightBigIcon />,
|
||||||
|
preconditionStrategy: () => {
|
||||||
|
return pageMode !== 'page';
|
||||||
|
},
|
||||||
|
label: () => t['com.affine.cmdk.affine.navigation.goto-page-list'](),
|
||||||
|
run() {
|
||||||
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||||
|
setPageMode('page');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:goto-edgeless-list',
|
||||||
|
category: 'affine:navigation',
|
||||||
|
icon: <ArrowRightBigIcon />,
|
||||||
|
preconditionStrategy: () => {
|
||||||
|
return pageMode !== 'edgeless';
|
||||||
|
},
|
||||||
|
label: () => t['com.affine.cmdk.affine.navigation.goto-edgeless-list'](),
|
||||||
|
run() {
|
||||||
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||||
|
setPageMode('edgeless');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:goto-workspace',
|
||||||
|
category: 'affine:navigation',
|
||||||
|
icon: <ArrowRightBigIcon />,
|
||||||
|
label: () => t['com.affine.cmdk.affine.navigation.goto-workspace'](),
|
||||||
|
run() {
|
||||||
|
store.set(openWorkspaceListModalAtom, true);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -56,6 +109,7 @@ export function registerAffineNavigationCommands({
|
|||||||
label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](),
|
label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](),
|
||||||
run() {
|
run() {
|
||||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
|
||||||
|
setPageMode('all');
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { commandScore } from '@affine/cmdk';
|
import { commandScore } from '@affine/cmdk';
|
||||||
|
import { useCollectionManager } from '@affine/component/page-list';
|
||||||
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||||
import type { Page, PageMeta } from '@blocksuite/store';
|
import type { Page, PageMeta } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
@@ -33,6 +35,8 @@ import {
|
|||||||
} from '../../../atoms';
|
} from '../../../atoms';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../../../shared';
|
||||||
|
import { currentCollectionsAtom } from '../../../utils/user-setting';
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
import type { CMDKCommand, CommandContext } from './types';
|
import type { CMDKCommand, CommandContext } from './types';
|
||||||
|
|
||||||
@@ -203,8 +207,11 @@ export const usePageCommands = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
results = pages.map(page => {
|
results = pages.map(page => {
|
||||||
|
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
||||||
|
const category =
|
||||||
|
pageMode === 'edgeless' ? 'affine:edgeless' : 'affine:pages';
|
||||||
const command = pageToCommand(
|
const command = pageToCommand(
|
||||||
'affine:pages',
|
category,
|
||||||
page,
|
page,
|
||||||
store,
|
store,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
@@ -280,15 +287,86 @@ export const usePageCommands = () => {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const collectionToCommand = (
|
||||||
|
collection: Collection,
|
||||||
|
store: ReturnType<typeof getCurrentStore>,
|
||||||
|
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||||
|
selectCollection: ReturnType<typeof useCollectionManager>['selectCollection'],
|
||||||
|
t: ReturnType<typeof useAFFiNEI18N>
|
||||||
|
): CMDKCommand => {
|
||||||
|
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
|
||||||
|
const label = collection.name || t['Untitled']();
|
||||||
|
const category = 'affine:collections';
|
||||||
|
return {
|
||||||
|
id: collection.id,
|
||||||
|
label: label,
|
||||||
|
// hack: when comparing, the part between >>> and <<< will be ignored
|
||||||
|
// adding this patch so that CMDK will not complain about duplicated commands
|
||||||
|
value:
|
||||||
|
label +
|
||||||
|
valueWrapperStart +
|
||||||
|
collection.id +
|
||||||
|
'.' +
|
||||||
|
category +
|
||||||
|
valueWrapperEnd,
|
||||||
|
originalValue: label,
|
||||||
|
category: category,
|
||||||
|
run: () => {
|
||||||
|
if (!currentWorkspaceId) {
|
||||||
|
console.error('current workspace not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigationHelper.jumpToSubPath(currentWorkspaceId, WorkspaceSubPath.ALL);
|
||||||
|
selectCollection(collection.id);
|
||||||
|
},
|
||||||
|
icon: <ViewLayersIcon />,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCollectionsCommands = () => {
|
||||||
|
// todo: considering collections for searching pages
|
||||||
|
const { savedCollections, selectCollection } = useCollectionManager(
|
||||||
|
currentCollectionsAtom
|
||||||
|
);
|
||||||
|
const store = getCurrentStore();
|
||||||
|
const query = useAtomValue(cmdkQueryAtom);
|
||||||
|
const navigationHelper = useNavigateHelper();
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
let results: CMDKCommand[] = [];
|
||||||
|
if (query.trim() === '') {
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
results = savedCollections.map(collection => {
|
||||||
|
const command = collectionToCommand(
|
||||||
|
collection,
|
||||||
|
store,
|
||||||
|
navigationHelper,
|
||||||
|
selectCollection,
|
||||||
|
t
|
||||||
|
);
|
||||||
|
return command;
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}, [query, savedCollections, store, navigationHelper, selectCollection, t]);
|
||||||
|
};
|
||||||
|
|
||||||
export const useCMDKCommandGroups = () => {
|
export const useCMDKCommandGroups = () => {
|
||||||
const pageCommands = usePageCommands();
|
const pageCommands = usePageCommands();
|
||||||
|
const collectionCommands = useCollectionsCommands();
|
||||||
const affineCommands = useAtomValue(filteredAffineCommands);
|
const affineCommands = useAtomValue(filteredAffineCommands);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const commands = [...pageCommands, ...affineCommands];
|
const commands = [
|
||||||
|
...pageCommands,
|
||||||
|
...collectionCommands,
|
||||||
|
...affineCommands,
|
||||||
|
];
|
||||||
const groups = groupBy(commands, command => command.category);
|
const groups = groupBy(commands, command => command.category);
|
||||||
return Object.entries(groups) as [CommandCategory, CMDKCommand[]][];
|
return Object.entries(groups) as [CommandCategory, CMDKCommand[]][];
|
||||||
}, [affineCommands, pageCommands]);
|
}, [affineCommands, collectionCommands, pageCommands]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const customCommandFilter = (value: string, search: string) => {
|
export const customCommandFilter = (value: string, search: string) => {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const categoryToI18nKey: Record<CommandCategory, i18nKey> = {
|
|||||||
'affine:general': 'com.affine.cmdk.affine.category.affine.general',
|
'affine:general': 'com.affine.cmdk.affine.category.affine.general',
|
||||||
'affine:layout': 'com.affine.cmdk.affine.category.affine.layout',
|
'affine:layout': 'com.affine.cmdk.affine.category.affine.layout',
|
||||||
'affine:pages': 'com.affine.cmdk.affine.category.affine.pages',
|
'affine:pages': 'com.affine.cmdk.affine.category.affine.pages',
|
||||||
|
'affine:edgeless': 'com.affine.cmdk.affine.category.affine.edgeless',
|
||||||
|
'affine:collections': 'com.affine.cmdk.affine.category.affine.collections',
|
||||||
'affine:settings': 'com.affine.cmdk.affine.category.affine.settings',
|
'affine:settings': 'com.affine.cmdk.affine.category.affine.settings',
|
||||||
'affine:updates': 'com.affine.cmdk.affine.category.affine.updates',
|
'affine:updates': 'com.affine.cmdk.affine.category.affine.updates',
|
||||||
'affine:help': 'com.affine.cmdk.affine.category.affine.help',
|
'affine:help': 'com.affine.cmdk.affine.category.affine.help',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const WorkspaceModeFilterTab = () => {
|
|||||||
return (
|
return (
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
width={300}
|
width={300}
|
||||||
defaultValue={value}
|
value={value}
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
>
|
>
|
||||||
<RadioButton value="all" style={{ textTransform: 'capitalize' }}>
|
<RadioButton value="all" style={{ textTransform: 'capitalize' }}>
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ import {
|
|||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import { Menu } from '@toeverything/components/menu';
|
import { Menu } from '@toeverything/components/menu';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import type { HTMLAttributes, ReactElement } from 'react';
|
import type { HTMLAttributes, ReactElement } from 'react';
|
||||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
import { forwardRef, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { openWorkspaceListModalAtom } from '../../atoms';
|
||||||
import { useHistoryAtom } from '../../atoms/history';
|
import { useHistoryAtom } from '../../atoms/history';
|
||||||
import { useAppSetting } from '../../atoms/settings';
|
import { useAppSetting } from '../../atoms/settings';
|
||||||
import type { AllWorkspace } from '../../shared';
|
import type { AllWorkspace } from '../../shared';
|
||||||
@@ -100,7 +101,9 @@ export const RootAppSidebar = ({
|
|||||||
const { backToAll } = useCollectionManager(currentCollectionsAtom);
|
const { backToAll } = useCollectionManager(currentCollectionsAtom);
|
||||||
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useState(false);
|
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
|
||||||
|
openWorkspaceListModalAtom
|
||||||
|
);
|
||||||
const onClickNewPage = useCallback(async () => {
|
const onClickNewPage = useCallback(async () => {
|
||||||
const page = createPage();
|
const page = createPage();
|
||||||
await page.waitForLoaded();
|
await page.waitForLoaded();
|
||||||
@@ -142,7 +145,7 @@ export const RootAppSidebar = ({
|
|||||||
});
|
});
|
||||||
const closeUserWorkspaceList = useCallback(() => {
|
const closeUserWorkspaceList = useCallback(() => {
|
||||||
setOpenUserWorkspaceList(false);
|
setOpenUserWorkspaceList(false);
|
||||||
}, []);
|
}, [setOpenUserWorkspaceList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -178,7 +181,7 @@ export const RootAppSidebar = ({
|
|||||||
currentWorkspace={currentWorkspace}
|
currentWorkspace={currentWorkspace}
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setOpenUserWorkspaceList(true);
|
setOpenUserWorkspaceList(true);
|
||||||
}, [])}
|
}, [setOpenUserWorkspaceList])}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
<QuickSearchInput
|
<QuickSearchInput
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useStore } from 'jotai';
|
import { useAtom, useStore } from 'jotai';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { allPageModeSelectAtom } from '../atoms';
|
||||||
import {
|
import {
|
||||||
registerAffineCreationCommands,
|
registerAffineCreationCommands,
|
||||||
registerAffineLayoutCommands,
|
registerAffineLayoutCommands,
|
||||||
@@ -20,6 +21,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
|
const [pageMode, setPageMode] = useAtom(allPageModeSelectAtom);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
@@ -28,6 +30,8 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
t,
|
t,
|
||||||
workspace: currentWorkspace.blockSuiteWorkspace,
|
workspace: currentWorkspace.blockSuiteWorkspace,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
|
pageMode,
|
||||||
|
setPageMode,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
|
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
|
||||||
@@ -50,5 +54,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
theme,
|
theme,
|
||||||
currentWorkspace.blockSuiteWorkspace,
|
currentWorkspace.blockSuiteWorkspace,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
|
pageMode,
|
||||||
|
setPageMode,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,5 +609,11 @@
|
|||||||
"com.affine.cmdk.affine.category.editor.insert-object": "Insert Object",
|
"com.affine.cmdk.affine.category.editor.insert-object": "Insert Object",
|
||||||
"com.affine.cmdk.affine.category.editor.page": "Page Commands",
|
"com.affine.cmdk.affine.category.editor.page": "Page Commands",
|
||||||
"com.affine.cmdk.affine.category.editor.edgeless": "Edgeless Commands",
|
"com.affine.cmdk.affine.category.editor.edgeless": "Edgeless Commands",
|
||||||
"com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation"
|
"com.affine.cmdk.affine.editor.edgeless.presentation-start": "Start Presentation",
|
||||||
|
"com.affine.cmdk.affine.navigation.goto-page-list": "Go to Page List",
|
||||||
|
"com.affine.cmdk.affine.navigation.goto-edgeless-list": "Go to Edgeless List",
|
||||||
|
"com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace",
|
||||||
|
"com.affine.cmdk.affine.category.affine.edgeless": "Edgeless",
|
||||||
|
"com.affine.cmdk.affine.category.affine.collections": "Collections",
|
||||||
|
"com.affine.cmdk.affine.import-workspace": "Import Workspace"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export type CommandCategory =
|
|||||||
| 'editor:edgeless'
|
| 'editor:edgeless'
|
||||||
| 'affine:recent'
|
| 'affine:recent'
|
||||||
| 'affine:pages'
|
| 'affine:pages'
|
||||||
|
| 'affine:edgeless'
|
||||||
|
| 'affine:collections'
|
||||||
| 'affine:navigation'
|
| 'affine:navigation'
|
||||||
| 'affine:creation'
|
| 'affine:creation'
|
||||||
| 'affine:settings'
|
| 'affine:settings'
|
||||||
|
|||||||
Reference in New Issue
Block a user