mirror of
https://github.com/Mail-0/Zero.git
synced 2026-07-01 08:16:28 +00:00
reply ui upgrade
This commit is contained in:
@@ -289,6 +289,7 @@ export function CreateEmail() {
|
||||
initialValue={defaultValue}
|
||||
onChange={(newContent) => setMessageContent(newContent)}
|
||||
key={resetEditorKey}
|
||||
placeholder="Write your message here..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
/* Only show placeholder when editor is completely empty */
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
color: #c2c8d0;
|
||||
content: "Start writing...";
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Style for all empty paragraphs */
|
||||
.ProseMirror p.is-empty::before {
|
||||
color: #c2c8d0;
|
||||
content: "Continue writing...";
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Remove the style for all empty paragraphs */
|
||||
|
||||
@@ -19,7 +19,8 @@ import { uploadFn } from "@/components/create/image-upload";
|
||||
import { handleImageDrop, handleImagePaste } from "novel";
|
||||
import EditorMenu from "@/components/create/editor-menu";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useReducer } from "react";
|
||||
import { useReducer, useRef } from "react";
|
||||
import "./editor.css";
|
||||
|
||||
const hljs = require("highlight.js");
|
||||
|
||||
@@ -38,6 +39,7 @@ export const defaultEditorContent = {
|
||||
interface EditorProps {
|
||||
initialValue?: JSONContent;
|
||||
onChange: (content: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface EditorState {
|
||||
@@ -68,24 +70,35 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState {
|
||||
}
|
||||
}
|
||||
|
||||
export default function Editor({ initialValue, onChange }: EditorProps) {
|
||||
export default function Editor({ initialValue, onChange, placeholder = "Write something..." }: EditorProps) {
|
||||
const [state, dispatch] = useReducer(editorReducer, {
|
||||
openNode: false,
|
||||
openColor: false,
|
||||
openLink: false,
|
||||
openAI: false,
|
||||
});
|
||||
|
||||
// Add a ref to store the editor content to prevent losing it on refresh
|
||||
const contentRef = useRef<string>("");
|
||||
|
||||
const { openNode, openColor, openLink, openAI } = state;
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-[220px] sm:max-w-[400px]">
|
||||
<div
|
||||
className="relative w-full max-w-[450px] sm:max-w-[600px]"
|
||||
onKeyDown={(e) => {
|
||||
// Prevent form submission on Enter key
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EditorRoot>
|
||||
<EditorContent
|
||||
immediatelyRender={false}
|
||||
initialContent={initialValue}
|
||||
extensions={extensions}
|
||||
className="min-h-96 max-w-[220px] sm:max-w-[400px]"
|
||||
className="min-h-96 max-w-[450px] sm:max-w-[600px]"
|
||||
editorProps={{
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => handleCommandNavigation(event),
|
||||
@@ -96,14 +109,28 @@ export default function Editor({ initialValue, onChange }: EditorProps) {
|
||||
attributes: {
|
||||
class:
|
||||
"prose dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full",
|
||||
"data-placeholder": placeholder,
|
||||
},
|
||||
}}
|
||||
onUpdate={({ editor }) => {
|
||||
// Store the content in the ref to prevent losing it
|
||||
contentRef.current = editor.getHTML();
|
||||
onChange(editor.getHTML());
|
||||
}}
|
||||
slotAfter={<ImageResizer />}
|
||||
>
|
||||
<EditorCommand className="border-muted bg-background z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border px-1 py-2 shadow-md transition-all">
|
||||
{/* Make sure the command palette doesn't cause a refresh */}
|
||||
<EditorCommand
|
||||
className="border-muted bg-background z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border px-1 py-2 shadow-md transition-all"
|
||||
onKeyDown={(e) => {
|
||||
// Prevent form submission on any key that might trigger it
|
||||
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Rest of the command palette */}
|
||||
<EditorCommandEmpty className="text-muted-foreground px-2">
|
||||
No results
|
||||
</EditorCommandEmpty>
|
||||
@@ -111,7 +138,11 @@ export default function Editor({ initialValue, onChange }: EditorProps) {
|
||||
{suggestionItems.map((item) => (
|
||||
<EditorCommandItem
|
||||
value={item.title}
|
||||
onCommand={(val) => item.command?.(val)}
|
||||
onCommand={(val) => {
|
||||
// Prevent default behavior that might cause refresh
|
||||
item.command?.(val);
|
||||
return false;
|
||||
}}
|
||||
className="hover:bg-accent aria-selected:bg-accent flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-[10px]"
|
||||
key={item.title}
|
||||
>
|
||||
@@ -127,6 +158,7 @@ export default function Editor({ initialValue, onChange }: EditorProps) {
|
||||
</EditorCommandList>
|
||||
</EditorCommand>
|
||||
|
||||
{/* Rest of the editor menu */}
|
||||
<EditorMenu
|
||||
open={openAI}
|
||||
onOpenChange={(open) => dispatch({ type: "TOGGLE_AI", payload: open })}
|
||||
|
||||
@@ -237,11 +237,11 @@ export function Mail({ folder }: MailProps) {
|
||||
<>
|
||||
<ResizableHandle className="opacity-0" />
|
||||
<ResizablePanel
|
||||
className="shadow-sm md:flex md:rounded-2xl md:border md:shadow-sm"
|
||||
className="shadow-sm md:flex md:rounded-2xl md:border md:shadow-sm bg-offsetLight dark:bg-offsetDark"
|
||||
defaultSize={75}
|
||||
minSize={25}
|
||||
>
|
||||
<div className="hidden h-[calc(100vh-(12px+14px))] flex-1 md:block">
|
||||
<div className="hidden h-[calc(100vh-(12px+14px))] flex-1 md:block relative top-2">
|
||||
<ThreadDisplay mail={mail.selected} onClose={handleClose} />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { FileIcon, Paperclip, Reply, Send, X } from "lucide-react";
|
||||
import { ArrowUp, FileIcon, Paperclip, Reply, Send, X, Plus } from "lucide-react";
|
||||
import { cleanEmailAddress, truncateFileName } from "@/lib/utils";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import Editor from "@/components/create/editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { sendEmail } from "@/actions/send";
|
||||
import { useRef, useState } from "react";
|
||||
import { ParsedMessage } from "@/types";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { JSONContent } from "novel";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { toast } from "sonner";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[] }) {
|
||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -17,6 +21,8 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [messageContent, setMessageContent] = useState("");
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
|
||||
const [isComposerOpen, setIsComposerOpen] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const handleAttachment = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
@@ -37,6 +43,34 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
const handleFocus = () => setIsTextAreaFocused(true);
|
||||
const handleBlur = () => setIsTextAreaFocused(false);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (e.dataTransfer.files) {
|
||||
setAttachments([...attachments, ...Array.from(e.dataTransfer.files)]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const constructReplyBody = (
|
||||
formattedMessage: string,
|
||||
originalDate: string,
|
||||
@@ -76,10 +110,7 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
const messageId = emailData[0]?.messageId;
|
||||
const threadId = emailData[0]?.threadId;
|
||||
|
||||
const formattedMessage = messageContent
|
||||
.split("\n")
|
||||
.map((line) => `<div>${line || "<br/>"}</div>`)
|
||||
.join("");
|
||||
const formattedMessage = messageContent;
|
||||
|
||||
const replyBody = constructReplyBody(
|
||||
formattedMessage,
|
||||
@@ -109,6 +140,7 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
});
|
||||
|
||||
setMessageContent("");
|
||||
setIsComposerOpen(false);
|
||||
toast.success("Email sent successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
@@ -116,15 +148,69 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
}
|
||||
};
|
||||
|
||||
const toggleComposer = () => {
|
||||
setIsComposerOpen(!isComposerOpen);
|
||||
if (!isComposerOpen) {
|
||||
setTimeout(() => {
|
||||
editorRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the message is empty
|
||||
const isMessageEmpty = !messageContent || messageContent === JSON.stringify({
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Check if form is valid for submission
|
||||
const isFormValid = !isMessageEmpty || attachments.length > 0;
|
||||
|
||||
if (!isComposerOpen) {
|
||||
return (
|
||||
<div className="bg-offsetLight dark:bg-offsetDark w-full p-2">
|
||||
<Button
|
||||
onClick={toggleComposer}
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-md"
|
||||
variant="outline"
|
||||
>
|
||||
<Reply className="h-4 w-4" />
|
||||
<span>Reply to {emailData[emailData.length - 1]?.sender?.name || "this email"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full bg-offsetLight p-2 dark:bg-offsetDark">
|
||||
<div className="bg-offsetLight dark:bg-offsetDark w-full p-2">
|
||||
<form
|
||||
className={cn(
|
||||
"flex h-72 flex-col space-y-2.5 rounded-[10px] border border-border px-2 py-4",
|
||||
"border-border flex h-[300px] flex-col space-y-2.5 rounded-[10px] border px-2 py-4 relative",
|
||||
isTextAreaFocused ? "ring-2 ring-[#3D3D3D]" : "",
|
||||
)}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onSubmit={(e) => {
|
||||
// Prevent default form submission
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
{isDragging && (
|
||||
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center border-2 border-dashed border-primary/30 rounded-2xl m-4">
|
||||
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
||||
<Paperclip className="h-12 w-12 text-muted-foreground" />
|
||||
<p className="text-lg font-medium">Drop files to attach</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Reply className="h-4 w-4" />
|
||||
<p className="truncate">
|
||||
@@ -132,105 +218,127 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
{emailData[emailData.length - 1]?.sender?.email})
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleComposer();
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
ref={editorRef}
|
||||
className="min-h-[40px] w-full flex-grow resize-none rounded-2xl border-0 bg-transparent leading-relaxed placeholder:text-muted-foreground/70 focus:outline-none md:text-base"
|
||||
placeholder="Write your reply..."
|
||||
spellCheck={true}
|
||||
value={messageContent}
|
||||
onChange={(e) => {
|
||||
setMessageContent(e.target.value);
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
{(attachments.length > 0 || isUploading) && (
|
||||
<div className="relative z-50 min-h-[32px]">
|
||||
<div className="hide-scrollbar absolute inset-x-0 flex gap-2 overflow-x-auto">
|
||||
{isUploading && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="inline-flex shrink-0 animate-pulse items-center bg-background/50 px-2 py-1.5 text-xs"
|
||||
>
|
||||
Uploading...
|
||||
</Badge>
|
||||
)}
|
||||
{attachments.map((file, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="inline-flex shrink-0 items-center gap-1 bg-background/50 px-2 py-1.5 text-xs"
|
||||
>
|
||||
<span className="max-w-[120px] truncate">{truncateFileName(file.name)}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-1 h-4 w-4 hover:bg-background/80"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeAttachment(index);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-64 p-0">
|
||||
<div className="relative h-32 w-full">
|
||||
{file.type.startsWith("image/") ? (
|
||||
<Image
|
||||
src={URL.createObjectURL(file) || "/placeholder.svg"}
|
||||
alt={file.name}
|
||||
fill
|
||||
className="rounded-t-md object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center p-4">
|
||||
<FileIcon className="h-16 w-16 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-secondary p-2">
|
||||
<p className="text-sm font-medium">{truncateFileName(file.name, 30)}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Size: {(file.size / (1024 * 1024)).toFixed(2)} MB
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last modified: {new Date(file.lastModified).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full flex-grow overflow-hidden p-1">
|
||||
<div
|
||||
className=" h-full w-full"
|
||||
onDragOver={(e) => e.stopPropagation()}
|
||||
onDragLeave={(e) => e.stopPropagation()}
|
||||
onDrop={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Editor
|
||||
onChange={(content) => {
|
||||
setMessageContent(content);
|
||||
}}
|
||||
initialValue={{
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [],
|
||||
},
|
||||
],
|
||||
}}
|
||||
placeholder="Type your reply here..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
className="h-8 w-8 hover:bg-background/80"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById("attachment-input")?.click();
|
||||
}}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
<span className="sr-only">Add attachment</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Attach file</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="group relative w-9 overflow-hidden transition-all duration-200 hover:w-32"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById("attachment-input")?.click();
|
||||
}}
|
||||
>
|
||||
<Plus className="absolute left-[9px] h-6 w-6" />
|
||||
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
Attachments
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{attachments.length > 0 && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
<span>
|
||||
{attachments.length} attachment{attachments.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 touch-auto" align="start">
|
||||
<div className="space-y-2">
|
||||
<div className="px-1">
|
||||
<h4 className="font-medium leading-none">Attachments</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{attachments.length} file{attachments.length !== 1 ? "s" : ""} attached
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="h-[300px] touch-auto overflow-y-auto overscroll-contain px-1 py-1">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{attachments.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative overflow-hidden rounded-md border"
|
||||
>
|
||||
<div className="relative h-24 w-full overflow-hidden bg-muted">
|
||||
{file.type.startsWith("image/") ? (
|
||||
<Image
|
||||
src={URL.createObjectURL(file) || "/placeholder.svg"}
|
||||
alt={file.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<FileIcon className="text-primary h-10 w-10" />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1 h-6 w-6 rounded-full bg-background/80 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
removeAttachment(index);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-muted/10 p-2">
|
||||
<p className="text-xs font-medium">
|
||||
{truncateFileName(file.name, 20)}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{(file.size / (1024 * 1024)).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
id="attachment-input"
|
||||
@@ -244,8 +352,20 @@ export default function ReplyCompose({ emailData }: { emailData: ParsedMessage[]
|
||||
<Button variant="ghost" size="sm" className="h-8">
|
||||
Save draft
|
||||
</Button>
|
||||
<Button size="sm" className="h-8" onClick={handleSendEmail}>
|
||||
Send
|
||||
<Button
|
||||
size="sm"
|
||||
className="group relative h-8 w-9 overflow-hidden transition-all duration-200 hover:w-24"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSendEmail(e);
|
||||
}}
|
||||
disabled={!isFormValid}
|
||||
type="button"
|
||||
>
|
||||
<ArrowUp className="absolute left-2.5 h-4 w-4" />
|
||||
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
Send
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user