Make adding projects discoverable in new workspaces (#2026)
* fix(app): make project setup discoverable in new workspaces * fix(app): keep project picker actions outside options
This commit is contained in:
@@ -35,6 +35,8 @@ import {
|
|||||||
hasGithubAuth,
|
hasGithubAuth,
|
||||||
} from "./helpers/github-fixtures";
|
} from "./helpers/github-fixtures";
|
||||||
import { getServerId } from "./helpers/server-id";
|
import { getServerId } from "./helpers/server-id";
|
||||||
|
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||||
|
import { seedSavedSettingsHosts } from "./helpers/settings";
|
||||||
import {
|
import {
|
||||||
expectSidebarWorkspaceSelected,
|
expectSidebarWorkspaceSelected,
|
||||||
expectWorkspaceHeader,
|
expectWorkspaceHeader,
|
||||||
@@ -209,6 +211,53 @@ test.describe("New workspace flow", () => {
|
|||||||
await client?.close().catch(() => undefined);
|
await client?.close().catch(() => undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("adds a project from the selected empty host", async ({ page }) => {
|
||||||
|
const repo = await createTempGitRepo("new-workspace-project-picker-");
|
||||||
|
const primaryServerId = getServerId();
|
||||||
|
const emptyServerId = "empty-new-workspace-host";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const openedProject = await openProjectViaDaemon(client, repo.path);
|
||||||
|
localWorkspaceIds.add(openedProject.workspaceId);
|
||||||
|
await seedSavedSettingsHosts(page, [
|
||||||
|
{
|
||||||
|
serverId: primaryServerId,
|
||||||
|
label: "Primary host",
|
||||||
|
endpoint: `127.0.0.1:${getE2EDaemonPort()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverId: emptyServerId,
|
||||||
|
label: "Empty host",
|
||||||
|
endpoint: "127.0.0.1:9",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await waitForSidebarHydration(page);
|
||||||
|
await openGlobalNewWorkspaceComposer(page);
|
||||||
|
|
||||||
|
const projectTrigger = page.getByTestId("new-workspace-project-picker-trigger");
|
||||||
|
await projectTrigger.click();
|
||||||
|
await page.getByPlaceholder("Search projects").fill("no matching project");
|
||||||
|
await expect(page.getByTestId("new-workspace-project-picker-add-project")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
|
await page.getByTestId("host-picker-trigger").click();
|
||||||
|
await page.getByTestId(`new-workspace-host-picker-option-${emptyServerId}`).click();
|
||||||
|
await expect(projectTrigger).toContainText("Choose project");
|
||||||
|
await projectTrigger.click();
|
||||||
|
|
||||||
|
const addProject = page.getByTestId("new-workspace-project-picker-add-project");
|
||||||
|
await expect(addProject).toContainText("Add project");
|
||||||
|
await expect(addProject).toContainText(/(?:⌘|Ctrl\+)O/);
|
||||||
|
await addProject.click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId("project-picker-input")).toBeVisible();
|
||||||
|
} finally {
|
||||||
|
await repo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("sidebar workspace navigation updates URL and header", async ({ page }) => {
|
test("sidebar workspace navigation updates URL and header", async ({ page }) => {
|
||||||
const serverId = getServerId();
|
const serverId = getServerId();
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ export interface ComboboxProps {
|
|||||||
desktopFixedHeight?: number;
|
desktopFixedHeight?: number;
|
||||||
/** Content rendered above the scroll area on desktop (sticky header). */
|
/** Content rendered above the scroll area on desktop (sticky header). */
|
||||||
stickyHeader?: ReactNode;
|
stickyHeader?: ReactNode;
|
||||||
|
/** Content rendered below the scroll area. */
|
||||||
|
footer?: ReactNode;
|
||||||
/** When true, selecting an option does not close the picker (multi-select mode). */
|
/** When true, selecting an option does not close the picker (multi-select mode). */
|
||||||
keepOpenOnSelect?: boolean;
|
keepOpenOnSelect?: boolean;
|
||||||
anchorRef: React.RefObject<View | null>;
|
anchorRef: React.RefObject<View | null>;
|
||||||
@@ -928,6 +930,7 @@ interface MobileBodyProps {
|
|||||||
header: SheetHeader | undefined;
|
header: SheetHeader | undefined;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
stickyHeader: ReactNode;
|
stickyHeader: ReactNode;
|
||||||
|
footer: ReactNode;
|
||||||
searchable: boolean;
|
searchable: boolean;
|
||||||
hasChildren: boolean;
|
hasChildren: boolean;
|
||||||
mobileChildrenScrollEnabled: boolean;
|
mobileChildrenScrollEnabled: boolean;
|
||||||
@@ -1026,6 +1029,7 @@ function MobileComboboxBody(props: MobileBodyProps): ReactElement {
|
|||||||
{body}
|
{body}
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
)}
|
)}
|
||||||
|
{props.footer ? <View style={styles.footer}>{props.footer}</View> : null}
|
||||||
</IsolatedBottomSheetModal>
|
</IsolatedBottomSheetModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1039,6 +1043,7 @@ interface DesktopBodyProps {
|
|||||||
handleDesktopContentLayout: (event: LayoutChangeEvent) => void;
|
handleDesktopContentLayout: (event: LayoutChangeEvent) => void;
|
||||||
header: SheetHeader | undefined;
|
header: SheetHeader | undefined;
|
||||||
stickyHeader: ReactNode;
|
stickyHeader: ReactNode;
|
||||||
|
footer: ReactNode;
|
||||||
searchable: boolean;
|
searchable: boolean;
|
||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -1192,6 +1197,7 @@ function DesktopComboboxBody(props: DesktopBodyProps): ReactElement {
|
|||||||
renderOption={props.renderOption}
|
renderOption={props.renderOption}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{props.footer ? <View style={styles.footer}>{props.footer}</View> : null}
|
||||||
</FloatingSurface>
|
</FloatingSurface>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -1224,6 +1230,7 @@ export function Combobox({
|
|||||||
desktopMinWidth,
|
desktopMinWidth,
|
||||||
desktopFixedHeight,
|
desktopFixedHeight,
|
||||||
stickyHeader,
|
stickyHeader,
|
||||||
|
footer,
|
||||||
keepOpenOnSelect = false,
|
keepOpenOnSelect = false,
|
||||||
anchorRef,
|
anchorRef,
|
||||||
children,
|
children,
|
||||||
@@ -1504,6 +1511,7 @@ export function Combobox({
|
|||||||
header={header}
|
header={header}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
stickyHeader={stickyHeader}
|
stickyHeader={stickyHeader}
|
||||||
|
footer={footer}
|
||||||
searchable={searchable}
|
searchable={searchable}
|
||||||
hasChildren={hasChildren}
|
hasChildren={hasChildren}
|
||||||
mobileChildrenScrollEnabled={mobileChildrenScrollEnabled}
|
mobileChildrenScrollEnabled={mobileChildrenScrollEnabled}
|
||||||
@@ -1537,6 +1545,7 @@ export function Combobox({
|
|||||||
handleDesktopContentLayout={handleDesktopContentLayout}
|
handleDesktopContentLayout={handleDesktopContentLayout}
|
||||||
header={header}
|
header={header}
|
||||||
stickyHeader={stickyHeader}
|
stickyHeader={stickyHeader}
|
||||||
|
footer={footer}
|
||||||
searchable={searchable}
|
searchable={searchable}
|
||||||
searchPlaceholder={effectiveSearchPlaceholder}
|
searchPlaceholder={effectiveSearchPlaceholder}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -1650,6 +1659,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.colors.border,
|
||||||
|
},
|
||||||
bottomSheetHeader: {
|
bottomSheetHeader: {
|
||||||
paddingHorizontal: theme.spacing[6],
|
paddingHorizontal: theme.spacing[6],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
|
|||||||
@@ -5,11 +5,19 @@ import type { TFunction } from "i18next";
|
|||||||
import { Pressable, Text, View } from "react-native";
|
import { Pressable, Text, View } from "react-native";
|
||||||
import type { PressableStateCallbackType } from "react-native";
|
import type { PressableStateCallbackType } from "react-native";
|
||||||
import ReanimatedAnimated from "react-native-reanimated";
|
import ReanimatedAnimated from "react-native-reanimated";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { createNameId } from "mnemonic-id";
|
import { createNameId } from "mnemonic-id";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Check, ChevronDown, Folder, GitBranch, GitPullRequest, X } from "lucide-react-native";
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
Folder,
|
||||||
|
FolderPlus,
|
||||||
|
GitBranch,
|
||||||
|
GitPullRequest,
|
||||||
|
X,
|
||||||
|
} from "lucide-react-native";
|
||||||
import { Composer } from "@/composer";
|
import { Composer } from "@/composer";
|
||||||
import { FileDropZone } from "@/components/file-drop/file-drop-zone";
|
import { FileDropZone } from "@/components/file-drop/file-drop-zone";
|
||||||
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||||
@@ -20,6 +28,7 @@ import { ProjectIconView } from "@/components/project-icon-view";
|
|||||||
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
||||||
import type { ComboboxOption as ComboboxOptionType, ComboboxProps } from "@/components/ui/combobox";
|
import type { ComboboxOption as ComboboxOptionType, ComboboxProps } from "@/components/ui/combobox";
|
||||||
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||||
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||||
@@ -43,6 +52,7 @@ import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-
|
|||||||
import { useWorkspace } from "@/stores/session-store-hooks";
|
import { useWorkspace } from "@/stores/session-store-hooks";
|
||||||
import { generateDraftId } from "@/stores/draft-keys";
|
import { generateDraftId } from "@/stores/draft-keys";
|
||||||
import { useDraftStore } from "@/stores/draft-store";
|
import { useDraftStore } from "@/stores/draft-store";
|
||||||
|
import { useProjectPickerStore } from "@/stores/project-picker-store";
|
||||||
import { isActiveCreateFlowForDraft, useCreateFlowStore } from "@/stores/create-flow-store";
|
import { isActiveCreateFlowForDraft, useCreateFlowStore } from "@/stores/create-flow-store";
|
||||||
import {
|
import {
|
||||||
useWorkspaceDraftSubmissionStore,
|
useWorkspaceDraftSubmissionStore,
|
||||||
@@ -50,6 +60,7 @@ import {
|
|||||||
} from "@/stores/workspace-draft-submission-store";
|
} from "@/stores/workspace-draft-submission-store";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
import { useFormPreferences } from "@/hooks/use-form-preferences";
|
import { useFormPreferences } from "@/hooks/use-form-preferences";
|
||||||
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
|
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
|
||||||
import { generateMessageId } from "@/types/stream";
|
import { generateMessageId } from "@/types/stream";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
@@ -63,6 +74,7 @@ import {
|
|||||||
type HostProjectListItem,
|
type HostProjectListItem,
|
||||||
} from "@/projects/host-projects";
|
} from "@/projects/host-projects";
|
||||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||||
|
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||||
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
||||||
import { useDraftWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
|
import { useDraftWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
|
||||||
import type { MessagePayload } from "@/composer/types";
|
import type { MessagePayload } from "@/composer/types";
|
||||||
@@ -87,6 +99,12 @@ import {
|
|||||||
} from "./new-workspace-initial-context";
|
} from "./new-workspace-initial-context";
|
||||||
import { useNewWorkspaceProjectPicker } from "./new-workspace/project-picker";
|
import { useNewWorkspaceProjectPicker } from "./new-workspace/project-picker";
|
||||||
|
|
||||||
|
const ThemedFolderPlus = withUnistyles(FolderPlus);
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
const addProjectIcon = (
|
||||||
|
<ThemedFolderPlus size={ICON_SIZE.sm} uniProps={foregroundMutedColorMapping} />
|
||||||
|
);
|
||||||
|
|
||||||
function resolveCheckoutRequest(
|
function resolveCheckoutRequest(
|
||||||
selectedItem: PickerItem | null,
|
selectedItem: PickerItem | null,
|
||||||
currentBranch: string | null,
|
currentBranch: string | null,
|
||||||
@@ -596,6 +614,25 @@ function NewWorkspaceProjectPickerOption({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AddProjectPickerAction({ onPress }: { onPress: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const openProjectKeys = useShortcutKeys("new-agent");
|
||||||
|
const shortcut = useMemo(
|
||||||
|
() => (openProjectKeys ? <Shortcut chord={openProjectKeys} /> : null),
|
||||||
|
[openProjectKeys],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboboxItem
|
||||||
|
testID="new-workspace-project-picker-add-project"
|
||||||
|
label={t("sidebar.actions.addProject")}
|
||||||
|
onPress={onPress}
|
||||||
|
leadingSlot={addProjectIcon}
|
||||||
|
trailingSlot={shortcut}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatPrLabel(item: { number: number; title: string }): string {
|
function formatPrLabel(item: { number: number; title: string }): string {
|
||||||
return `#${item.number} ${item.title}`;
|
return `#${item.number} ${item.title}`;
|
||||||
}
|
}
|
||||||
@@ -1357,6 +1394,7 @@ interface NewWorkspaceFormStackInput {
|
|||||||
iconDataByProjectKey: Map<string, string | null>;
|
iconDataByProjectKey: Map<string, string | null>;
|
||||||
selectedOptionId: string;
|
selectedOptionId: string;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
|
onAddProject: () => void;
|
||||||
renderOption: RefPickerRenderOption;
|
renderOption: RefPickerRenderOption;
|
||||||
};
|
};
|
||||||
host: FormPickerControl & {
|
host: FormPickerControl & {
|
||||||
@@ -1394,6 +1432,10 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host";
|
host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host";
|
||||||
const showHostControl = host.allHosts.length > 1;
|
const showHostControl = host.allHosts.length > 1;
|
||||||
const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation);
|
const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation);
|
||||||
|
const addProjectAction = useMemo(
|
||||||
|
() => <AddProjectPickerAction onPress={project.onAddProject} />,
|
||||||
|
[project.onAddProject],
|
||||||
|
);
|
||||||
|
|
||||||
const badgePressableStyle = useCallback(
|
const badgePressableStyle = useCallback(
|
||||||
({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [
|
({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||||
@@ -1410,7 +1452,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
<ProjectPickerTrigger
|
<ProjectPickerTrigger
|
||||||
pickerAnchorRef={project.anchorRef}
|
pickerAnchorRef={project.anchorRef}
|
||||||
onPress={project.open}
|
onPress={project.open}
|
||||||
disabled={isPending || project.options.length === 0}
|
disabled={isPending}
|
||||||
badgePressableStyle={badgePressableStyle}
|
badgePressableStyle={badgePressableStyle}
|
||||||
label={project.triggerLabel}
|
label={project.triggerLabel}
|
||||||
projectKey={project.selectedProject?.projectKey ?? null}
|
projectKey={project.selectedProject?.projectKey ?? null}
|
||||||
@@ -1435,6 +1477,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
anchorRef={project.anchorRef}
|
anchorRef={project.anchorRef}
|
||||||
emptyText="No projects available."
|
emptyText="No projects available."
|
||||||
renderOption={project.renderOption}
|
renderOption={project.renderOption}
|
||||||
|
footer={addProjectAction}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -1589,6 +1632,7 @@ export function NewWorkspaceScreen({
|
|||||||
const [manualPickerSelection, setManualPickerSelection] = useState<PickerSelection | null>(null);
|
const [manualPickerSelection, setManualPickerSelection] = useState<PickerSelection | null>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
const [projectPickerOpen, setProjectPickerOpen] = useState(false);
|
const [projectPickerOpen, setProjectPickerOpen] = useState(false);
|
||||||
|
const openAddProjectPicker = useProjectPickerStore((state) => state.open);
|
||||||
const [isolationPickerOpen, setIsolationPickerOpen] = useState(false);
|
const [isolationPickerOpen, setIsolationPickerOpen] = useState(false);
|
||||||
const [pickerSearchQuery, setPickerSearchQuery] = useState("");
|
const [pickerSearchQuery, setPickerSearchQuery] = useState("");
|
||||||
const [debouncedPickerSearchQuery, setDebouncedPickerSearchQuery] = useState("");
|
const [debouncedPickerSearchQuery, setDebouncedPickerSearchQuery] = useState("");
|
||||||
@@ -1623,7 +1667,6 @@ export function NewWorkspaceScreen({
|
|||||||
lastActiveProject,
|
lastActiveProject,
|
||||||
allowAllProjects: supportsWorkspaceMultiplicity,
|
allowAllProjects: supportsWorkspaceMultiplicity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectIconTargets = useMemo(
|
const projectIconTargets = useMemo(
|
||||||
() =>
|
() =>
|
||||||
projects.flatMap((project) => {
|
projects.flatMap((project) => {
|
||||||
@@ -1797,6 +1840,11 @@ export function NewWorkspaceScreen({
|
|||||||
[selectProjectOption],
|
[selectProjectOption],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAddProject = useCallback(() => {
|
||||||
|
setProjectPickerOpen(false);
|
||||||
|
openAddProjectPicker(selectedServerId);
|
||||||
|
}, [openAddProjectPicker, selectedServerId]);
|
||||||
|
|
||||||
const checkoutHintPrAttachment = useMemo(
|
const checkoutHintPrAttachment = useMemo(
|
||||||
() =>
|
() =>
|
||||||
findCheckoutHintPrAttachment({
|
findCheckoutHintPrAttachment({
|
||||||
@@ -2099,6 +2147,7 @@ export function NewWorkspaceScreen({
|
|||||||
iconDataByProjectKey: projectIconDataByProjectKey,
|
iconDataByProjectKey: projectIconDataByProjectKey,
|
||||||
selectedOptionId: selectedProjectOptionId,
|
selectedOptionId: selectedProjectOptionId,
|
||||||
onSelect: handleSelectProjectOption,
|
onSelect: handleSelectProjectOption,
|
||||||
|
onAddProject: handleAddProject,
|
||||||
openState: projectPickerOpen,
|
openState: projectPickerOpen,
|
||||||
onOpenChange: handleProjectPickerOpenChange,
|
onOpenChange: handleProjectPickerOpenChange,
|
||||||
renderOption: renderProjectOption,
|
renderOption: renderProjectOption,
|
||||||
|
|||||||
Reference in New Issue
Block a user