mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add filters to plus page and fix layout (#10320)
This commit is contained in:
		
							parent
							
								
									90a40d2509
								
							
						
					
					
						commit
						507c6afa2c
					
				@ -1,4 +1,5 @@
 | 
				
			|||||||
import { baseUrl } from "@/api/baseUrl";
 | 
					import { baseUrl } from "@/api/baseUrl";
 | 
				
			||||||
 | 
					import FilterCheckBox from "@/components/filter/FilterCheckBox";
 | 
				
			||||||
import { Button } from "@/components/ui/button";
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Dialog,
 | 
					  Dialog,
 | 
				
			||||||
@ -8,15 +9,37 @@ import {
 | 
				
			|||||||
  DialogHeader,
 | 
					  DialogHeader,
 | 
				
			||||||
  DialogTitle,
 | 
					  DialogTitle,
 | 
				
			||||||
} from "@/components/ui/dialog";
 | 
					} from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuLabel,
 | 
				
			||||||
 | 
					  DropdownMenuSeparator,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/dropdown-menu";
 | 
				
			||||||
import { Event } from "@/types/event";
 | 
					import { Event } from "@/types/event";
 | 
				
			||||||
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
import axios from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
import { useCallback, useState } from "react";
 | 
					import { useCallback, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { FaList, FaVideo } from "react-icons/fa";
 | 
				
			||||||
import useSWR from "swr";
 | 
					import useSWR from "swr";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function SubmitPlus() {
 | 
					export default function SubmitPlus() {
 | 
				
			||||||
 | 
					  // filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selectedCameras, setSelectedCameras] = useState<string[]>();
 | 
				
			||||||
 | 
					  const [selectedLabels, setSelectedLabels] = useState<string[]>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { data: events, mutate: refresh } = useSWR<Event[]>([
 | 
					  const { data: events, mutate: refresh } = useSWR<Event[]>([
 | 
				
			||||||
    "events",
 | 
					    "events",
 | 
				
			||||||
    { limit: 100, in_progress: 0, is_submitted: 0 },
 | 
					    {
 | 
				
			||||||
 | 
					      limit: 100,
 | 
				
			||||||
 | 
					      in_progress: 0,
 | 
				
			||||||
 | 
					      is_submitted: 0,
 | 
				
			||||||
 | 
					      cameras: selectedCameras ? selectedCameras.join(",") : null,
 | 
				
			||||||
 | 
					      labels: selectedLabels ? selectedLabels.join(",") : null,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  ]);
 | 
					  ]);
 | 
				
			||||||
  const [upload, setUpload] = useState<Event>();
 | 
					  const [upload, setUpload] = useState<Event>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -54,52 +77,270 @@ export default function SubmitPlus() {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="size-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2 overflow-auto">
 | 
					    <div className="size-full flex flex-col">
 | 
				
			||||||
      <Dialog
 | 
					      <PlusFilterGroup
 | 
				
			||||||
        open={upload != undefined}
 | 
					        selectedCameras={selectedCameras}
 | 
				
			||||||
        onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
 | 
					        setSelectedCameras={setSelectedCameras}
 | 
				
			||||||
      >
 | 
					        selectedLabels={selectedLabels}
 | 
				
			||||||
        <DialogContent className="md:max-w-4xl">
 | 
					        setSelectedLabels={setSelectedLabels}
 | 
				
			||||||
          <DialogHeader>
 | 
					      />
 | 
				
			||||||
            <DialogTitle>Submit To Frigate+</DialogTitle>
 | 
					      <div className="size-full flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
 | 
				
			||||||
            <DialogDescription>
 | 
					        <div className="w-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
 | 
				
			||||||
              Objects in locations you want to avoid are not false positives.
 | 
					          <Dialog
 | 
				
			||||||
              Submitting them as false positives will confuse the model.
 | 
					            open={upload != undefined}
 | 
				
			||||||
            </DialogDescription>
 | 
					            onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
 | 
				
			||||||
          </DialogHeader>
 | 
					 | 
				
			||||||
          <img
 | 
					 | 
				
			||||||
            className="flex-grow-0"
 | 
					 | 
				
			||||||
            src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
 | 
					 | 
				
			||||||
            alt={`${upload?.label}`}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <DialogFooter>
 | 
					 | 
				
			||||||
            <Button>Cancel</Button>
 | 
					 | 
				
			||||||
            <Button
 | 
					 | 
				
			||||||
              className="bg-success"
 | 
					 | 
				
			||||||
              onClick={() => onSubmitToPlus(false)}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              This is a {upload?.label}
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
            <Button variant="destructive" onClick={() => onSubmitToPlus(true)}>
 | 
					 | 
				
			||||||
              This is not a {upload?.label}
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
          </DialogFooter>
 | 
					 | 
				
			||||||
        </DialogContent>
 | 
					 | 
				
			||||||
      </Dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      {events?.map((event) => {
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            className="size-full rounded-2xl flex justify-center items-center bg-black cursor-pointer"
 | 
					 | 
				
			||||||
            onClick={() => setUpload(event)}
 | 
					 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <img
 | 
					            <DialogContent className="md:max-w-4xl">
 | 
				
			||||||
              className="aspect-video h-full object-contain rounded-2xl"
 | 
					              <DialogHeader>
 | 
				
			||||||
              src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
 | 
					                <DialogTitle>Submit To Frigate+</DialogTitle>
 | 
				
			||||||
            />
 | 
					                <DialogDescription>
 | 
				
			||||||
          </div>
 | 
					                  Objects in locations you want to avoid are not false
 | 
				
			||||||
        );
 | 
					                  positives. Submitting them as false positives will confuse the
 | 
				
			||||||
      })}
 | 
					                  model.
 | 
				
			||||||
 | 
					                </DialogDescription>
 | 
				
			||||||
 | 
					              </DialogHeader>
 | 
				
			||||||
 | 
					              <img
 | 
				
			||||||
 | 
					                className="flex-grow-0"
 | 
				
			||||||
 | 
					                src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
 | 
				
			||||||
 | 
					                alt={`${upload?.label}`}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <DialogFooter>
 | 
				
			||||||
 | 
					                <Button>Cancel</Button>
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  className="bg-success"
 | 
				
			||||||
 | 
					                  onClick={() => onSubmitToPlus(false)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  This is a {upload?.label}
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  variant="destructive"
 | 
				
			||||||
 | 
					                  onClick={() => onSubmitToPlus(true)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  This is not a {upload?.label}
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </DialogFooter>
 | 
				
			||||||
 | 
					            </DialogContent>
 | 
				
			||||||
 | 
					          </Dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {events?.map((event) => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className="w-full rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer"
 | 
				
			||||||
 | 
					                onClick={() => setUpload(event)}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                  className="aspect-video h-full object-contain rounded-2xl"
 | 
				
			||||||
 | 
					                  src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
 | 
				
			||||||
 | 
					                  loading="lazy"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PlusFilterGroupProps = {
 | 
				
			||||||
 | 
					  selectedCameras: string[] | undefined;
 | 
				
			||||||
 | 
					  setSelectedCameras: (cameras: string[] | undefined) => void;
 | 
				
			||||||
 | 
					  selectedLabels: string[] | undefined;
 | 
				
			||||||
 | 
					  setSelectedLabels: (cameras: string[] | undefined) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					function PlusFilterGroup({
 | 
				
			||||||
 | 
					  selectedCameras,
 | 
				
			||||||
 | 
					  setSelectedCameras,
 | 
				
			||||||
 | 
					  selectedLabels,
 | 
				
			||||||
 | 
					  setSelectedLabels,
 | 
				
			||||||
 | 
					}: PlusFilterGroupProps) {
 | 
				
			||||||
 | 
					  const { data: config } = useSWR<FrigateConfig>("config");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const allCameras = useMemo(() => {
 | 
				
			||||||
 | 
					    if (!config) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Object.keys(config.cameras);
 | 
				
			||||||
 | 
					  }, [config]);
 | 
				
			||||||
 | 
					  const allLabels = useMemo<string[]>(() => {
 | 
				
			||||||
 | 
					    if (!config) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const labels = new Set<string>();
 | 
				
			||||||
 | 
					    const cameras = selectedCameras || Object.keys(config.cameras);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cameras.forEach((camera) => {
 | 
				
			||||||
 | 
					      const cameraConfig = config.cameras[camera];
 | 
				
			||||||
 | 
					      cameraConfig.objects.track.forEach((label) => {
 | 
				
			||||||
 | 
					        if (!ATTRIBUTES.includes(label)) {
 | 
				
			||||||
 | 
					          labels.add(label);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [...labels].sort();
 | 
				
			||||||
 | 
					  }, [config, selectedCameras]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [open, setOpen] = useState<"none" | "camera" | "label">("none");
 | 
				
			||||||
 | 
					  const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
 | 
				
			||||||
 | 
					    undefined,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
 | 
				
			||||||
 | 
					    undefined,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="w-full h-16 flex justify-start gap-2 items-center">
 | 
				
			||||||
 | 
					      <DropdownMenu
 | 
				
			||||||
 | 
					        open={open == "camera"}
 | 
				
			||||||
 | 
					        onOpenChange={(open) => {
 | 
				
			||||||
 | 
					          if (!open) {
 | 
				
			||||||
 | 
					            setCurrentCameras(selectedCameras);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          setOpen(open ? "camera" : "none");
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					          <Button size="sm" className="mx-1 capitalize" variant="secondary">
 | 
				
			||||||
 | 
					            <FaVideo className="md:mr-[10px] text-muted-foreground" />
 | 
				
			||||||
 | 
					            <div className="hidden md:block">
 | 
				
			||||||
 | 
					              {selectedCameras == undefined
 | 
				
			||||||
 | 
					                ? "All Cameras"
 | 
				
			||||||
 | 
					                : `${selectedCameras.length} Cameras`}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					        <DropdownMenuContent>
 | 
				
			||||||
 | 
					          <DropdownMenuLabel className="flex justify-center">
 | 
				
			||||||
 | 
					            Filter Cameras
 | 
				
			||||||
 | 
					          </DropdownMenuLabel>
 | 
				
			||||||
 | 
					          <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					          <FilterCheckBox
 | 
				
			||||||
 | 
					            isChecked={currentCameras == undefined}
 | 
				
			||||||
 | 
					            label="All Cameras"
 | 
				
			||||||
 | 
					            onCheckedChange={(isChecked) => {
 | 
				
			||||||
 | 
					              if (isChecked) {
 | 
				
			||||||
 | 
					                setCurrentCameras(undefined);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					          {allCameras.map((item) => (
 | 
				
			||||||
 | 
					            <FilterCheckBox
 | 
				
			||||||
 | 
					              key={item}
 | 
				
			||||||
 | 
					              isChecked={currentCameras?.includes(item) ?? false}
 | 
				
			||||||
 | 
					              label={item.replaceAll("_", " ")}
 | 
				
			||||||
 | 
					              onCheckedChange={(isChecked) => {
 | 
				
			||||||
 | 
					                if (isChecked) {
 | 
				
			||||||
 | 
					                  const updatedCameras = currentCameras
 | 
				
			||||||
 | 
					                    ? [...currentCameras]
 | 
				
			||||||
 | 
					                    : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  updatedCameras.push(item);
 | 
				
			||||||
 | 
					                  setCurrentCameras(updatedCameras);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  const updatedCameras = currentCameras
 | 
				
			||||||
 | 
					                    ? [...currentCameras]
 | 
				
			||||||
 | 
					                    : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  // can not deselect the last item
 | 
				
			||||||
 | 
					                  if (updatedCameras.length > 1) {
 | 
				
			||||||
 | 
					                    updatedCameras.splice(updatedCameras.indexOf(item), 1);
 | 
				
			||||||
 | 
					                    setCurrentCameras(updatedCameras);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					          <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					          <div className="flex justify-center items-center">
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="select"
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                setSelectedCameras(currentCameras);
 | 
				
			||||||
 | 
					                setOpen("none");
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Apply
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </DropdownMenuContent>
 | 
				
			||||||
 | 
					      </DropdownMenu>
 | 
				
			||||||
 | 
					      <DropdownMenu
 | 
				
			||||||
 | 
					        open={open == "label"}
 | 
				
			||||||
 | 
					        onOpenChange={(open) => {
 | 
				
			||||||
 | 
					          if (!open) {
 | 
				
			||||||
 | 
					            setCurrentLabels(selectedLabels);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          setOpen(open ? "label" : "none");
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					          <Button size="sm" className="mx-1 capitalize" variant="secondary">
 | 
				
			||||||
 | 
					            <FaList className="md:mr-[10px] text-muted-foreground" />
 | 
				
			||||||
 | 
					            <div className="hidden md:block">
 | 
				
			||||||
 | 
					              {selectedLabels == undefined
 | 
				
			||||||
 | 
					                ? "All Labels"
 | 
				
			||||||
 | 
					                : `${selectedLabels.length} Labels`}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					        <DropdownMenuContent>
 | 
				
			||||||
 | 
					          <DropdownMenuLabel className="flex justify-center">
 | 
				
			||||||
 | 
					            Filter Labels
 | 
				
			||||||
 | 
					          </DropdownMenuLabel>
 | 
				
			||||||
 | 
					          <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					          <FilterCheckBox
 | 
				
			||||||
 | 
					            isChecked={currentLabels == undefined}
 | 
				
			||||||
 | 
					            label="All Labels"
 | 
				
			||||||
 | 
					            onCheckedChange={(isChecked) => {
 | 
				
			||||||
 | 
					              if (isChecked) {
 | 
				
			||||||
 | 
					                setCurrentLabels(undefined);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					          {allLabels.map((item) => (
 | 
				
			||||||
 | 
					            <FilterCheckBox
 | 
				
			||||||
 | 
					              key={item}
 | 
				
			||||||
 | 
					              isChecked={currentLabels?.includes(item) ?? false}
 | 
				
			||||||
 | 
					              label={item.replaceAll("_", " ")}
 | 
				
			||||||
 | 
					              onCheckedChange={(isChecked) => {
 | 
				
			||||||
 | 
					                if (isChecked) {
 | 
				
			||||||
 | 
					                  const updatedLabels = currentLabels ? [...currentLabels] : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  updatedLabels.push(item);
 | 
				
			||||||
 | 
					                  setCurrentLabels(updatedLabels);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  const updatedLabels = currentLabels ? [...currentLabels] : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  // can not deselect the last item
 | 
				
			||||||
 | 
					                  if (updatedLabels.length > 1) {
 | 
				
			||||||
 | 
					                    updatedLabels.splice(updatedLabels.indexOf(item), 1);
 | 
				
			||||||
 | 
					                    setCurrentLabels(updatedLabels);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					          <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					          <div className="flex justify-center items-center">
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="select"
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                setSelectedLabels(currentLabels);
 | 
				
			||||||
 | 
					                setOpen("none");
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Apply
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </DropdownMenuContent>
 | 
				
			||||||
 | 
					      </DropdownMenu>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user