mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 00:59:02 +03:00
@@ -108,11 +108,10 @@ body {
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 3em 1em 0;
|
||||
padding: 3.3em 1em 0;
|
||||
}
|
||||
|
||||
.empty-folder {
|
||||
padding-top: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -202,6 +201,25 @@ body {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
height: calc(100vh - 5rem);
|
||||
border: 1px solid #ced4da;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
margin-left: auto;
|
||||
margin-right: 2em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.not-editable {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.path a {
|
||||
min-width: 400px;
|
||||
|
||||
@@ -7,59 +7,97 @@
|
||||
<link rel="icon" type="image/x-icon" href="__ASSERTS_PREFIX__favicon.ico">
|
||||
<link rel="stylesheet" href="__ASSERTS_PREFIX__index.css">
|
||||
<script>
|
||||
DATA = __INDEX_DATA__
|
||||
DATA = __INDEX_DATA__
|
||||
</script>
|
||||
<script src="__ASSERTS_PREFIX__index.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="head">
|
||||
<div class="breadcrumb"></div>
|
||||
<div class="toolbox">
|
||||
<div>
|
||||
<a href="?zip" class="zip-root hidden" title="Download folder as a .zip file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||||
<path
|
||||
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="" class="download hidden" title="Download file" download="">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||||
<path
|
||||
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control upload-file hidden" title="Upload files">
|
||||
<label for="file">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||||
<path
|
||||
d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
||||
</svg>
|
||||
</label>
|
||||
<input type="file" id="file" name="file" multiple>
|
||||
</div>
|
||||
<div class="control new-folder hidden" title="New folder">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="m.5 3 .04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.683.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z"/>
|
||||
<path d="M13.5 10a.5.5 0 0 1 .5.5V12h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V13h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path
|
||||
d="m.5 3 .04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.683.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z" />
|
||||
<path
|
||||
d="M13.5 10a.5.5 0 0 1 .5.5V12h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V13h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<form class="searchbar hidden">
|
||||
<div class="icon">
|
||||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/></svg>
|
||||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input id="search" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1">
|
||||
<input type="submit" hidden />
|
||||
</form>
|
||||
<div class="save-btn hidden" title="Save file">
|
||||
<svg viewBox="0 0 1024 1024" width="24" height="24">
|
||||
<path
|
||||
d="M426.666667 682.666667v42.666666h170.666666v-42.666666h-170.666666z m-42.666667-85.333334h298.666667v128h42.666666V418.133333L605.866667 298.666667H298.666667v426.666666h42.666666v-128h42.666667z m260.266667-384L810.666667 379.733333V810.666667H213.333333V213.333333h430.933334zM341.333333 341.333333h85.333334v170.666667H341.333333V341.333333z"
|
||||
fill="#444444" p-id="8311"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="empty-folder hidden"></div>
|
||||
<table class="uploaders-table hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell-name" colspan="2">Name</th>
|
||||
<th class="cell-status">Progress</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<table class="paths-table hidden">
|
||||
<thead>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="index-page hidden">
|
||||
<div class="empty-folder hidden"></div>
|
||||
<table class="uploaders-table hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell-name" colspan="2">Name</th>
|
||||
<th class="cell-status">Progress</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<table class="paths-table hidden">
|
||||
<thead>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="editor-page hidden">
|
||||
<div class="not-editable hidden"></div>
|
||||
<textarea class="editor hidden" cols="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener("DOMContentLoaded", ready);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
193
assets/index.js
193
assets/index.js
@@ -7,9 +7,14 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} DATA
|
||||
* @typedef {IndexDATA|EditDATA} DATA
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} IndexDATA
|
||||
* @property {string} href
|
||||
* @property {string} uri_prefix
|
||||
* @property {"Index"} kind
|
||||
* @property {PathItem[]} paths
|
||||
* @property {boolean} allow_upload
|
||||
* @property {boolean} allow_delete
|
||||
@@ -18,6 +23,14 @@
|
||||
* @property {boolean} dir_exists
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} EditDATA
|
||||
* @property {string} href
|
||||
* @property {string} uri_prefix
|
||||
* @property {"Edit"} kind
|
||||
* @property {string} editable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {DATA} DATA
|
||||
*/
|
||||
@@ -57,11 +70,43 @@ let $emptyFolder;
|
||||
/**
|
||||
* @type Element
|
||||
*/
|
||||
let $newFolder;
|
||||
/**
|
||||
* @type Element
|
||||
*/
|
||||
let $searchbar;
|
||||
let $editor;
|
||||
|
||||
function ready() {
|
||||
document.title = `Index of ${DATA.href} - Dufs`;
|
||||
$pathsTable = document.querySelector(".paths-table")
|
||||
$pathsTableHead = document.querySelector(".paths-table thead");
|
||||
$pathsTableBody = document.querySelector(".paths-table tbody");
|
||||
$uploadersTable = document.querySelector(".uploaders-table");
|
||||
$emptyFolder = document.querySelector(".empty-folder");
|
||||
$editor = document.querySelector(".editor");
|
||||
|
||||
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
||||
|
||||
if (DATA.kind == "Index") {
|
||||
|
||||
document.querySelector(".index-page").classList.remove("hidden");
|
||||
|
||||
if (DATA.allow_search) {
|
||||
setupSearch()
|
||||
}
|
||||
|
||||
if (DATA.allow_archive) {
|
||||
document.querySelector(".zip-root").classList.remove("hidden");
|
||||
}
|
||||
|
||||
renderPathsTableHead();
|
||||
renderPathsTableBody();
|
||||
|
||||
if (DATA.allow_upload) {
|
||||
dropzone();
|
||||
setupUpload();
|
||||
}
|
||||
} else if (DATA.kind == "Edit") {
|
||||
setupEditor();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Uploader {
|
||||
/**
|
||||
@@ -83,12 +128,12 @@ class Uploader {
|
||||
|
||||
upload() {
|
||||
const { idx, name } = this;
|
||||
const url = getUrl(name);
|
||||
const url = newUrl(name);
|
||||
const encodedName = encodedStr(name);
|
||||
$uploadersTable.insertAdjacentHTML("beforeend", `
|
||||
<tr id="upload${idx}" class="uploader">
|
||||
<td class="path cell-icon">
|
||||
${getSvg()}
|
||||
${getPathSvg()}
|
||||
</td>
|
||||
<td class="path cell-name">
|
||||
<a href="${url}">${encodedName}</a>
|
||||
@@ -105,7 +150,7 @@ class Uploader {
|
||||
|
||||
ajax() {
|
||||
Uploader.runnings += 1;
|
||||
const url = getUrl(this.name);
|
||||
const url = newUrl(this.name);
|
||||
this.lastUptime = Date.now();
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", e => this.progress(e), false);
|
||||
@@ -272,7 +317,7 @@ function renderPathsTableBody() {
|
||||
*/
|
||||
function addPath(file, index) {
|
||||
const encodedName = encodedStr(file.name);
|
||||
let url = getUrl(file.name)
|
||||
let url = newUrl(file.name)
|
||||
let actionDelete = "";
|
||||
let actionDownload = "";
|
||||
let actionMove = "";
|
||||
@@ -316,10 +361,10 @@ function addPath(file, index) {
|
||||
$pathsTableBody.insertAdjacentHTML("beforeend", `
|
||||
<tr id="addPath${index}">
|
||||
<td class="path cell-icon">
|
||||
${getSvg(file.path_type)}
|
||||
${getPathSvg(file.path_type)}
|
||||
</td>
|
||||
<td class="path cell-name">
|
||||
<a href="${url}">${encodedName}</a>
|
||||
<a href="${url}?edit" target="_blank">${encodedName}</a>
|
||||
</td>
|
||||
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
||||
<td class="cell-size">${formatSize(file.size).join(" ")}</td>
|
||||
@@ -339,19 +384,16 @@ async function deletePath(index) {
|
||||
if (!confirm(`Delete \`${file.name}\`?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(getUrl(file.name), {
|
||||
const res = await fetch(newUrl(file.name), {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
document.getElementById(`addPath${index}`).remove();
|
||||
DATA.paths[index] = null;
|
||||
if (!DATA.paths.find(v => !!v)) {
|
||||
$pathsTable.classList.add("hidden");
|
||||
$emptyFolder.textContent = dirEmptyNote;
|
||||
$emptyFolder.classList.remove("hidden");
|
||||
}
|
||||
} else {
|
||||
throw new Error(await res.text())
|
||||
await assertFetch(res);
|
||||
document.getElementById(`addPath${index}`).remove();
|
||||
DATA.paths[index] = null;
|
||||
if (!DATA.paths.find(v => !!v)) {
|
||||
$pathsTable.classList.add("hidden");
|
||||
$emptyFolder.textContent = dirEmptyNote;
|
||||
$emptyFolder.classList.remove("hidden");
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Cannot delete \`${file.name}\`, ${err.message}`);
|
||||
@@ -368,7 +410,7 @@ async function movePath(index) {
|
||||
const file = DATA.paths[index];
|
||||
if (!file) return;
|
||||
|
||||
const fileUrl = getUrl(file.name);
|
||||
const fileUrl = newUrl(file.name);
|
||||
const fileUrlObj = new URL(fileUrl)
|
||||
|
||||
const prefix = DATA.uri_prefix.slice(0, -1);
|
||||
@@ -388,11 +430,8 @@ async function movePath(index) {
|
||||
"Destination": newFileUrl,
|
||||
}
|
||||
});
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
location.href = newFileUrl.split("/").slice(0, -1).join("/")
|
||||
} else {
|
||||
throw new Error(await res.text())
|
||||
}
|
||||
await assertFetch(res);
|
||||
location.href = newFileUrl.split("/").slice(0, -1).join("/")
|
||||
} catch (err) {
|
||||
alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`);
|
||||
}
|
||||
@@ -426,12 +465,13 @@ function dropzone() {
|
||||
* Setup searchbar
|
||||
*/
|
||||
function setupSearch() {
|
||||
const $searchbar = document.querySelector(".searchbar");
|
||||
$searchbar.classList.remove("hidden");
|
||||
$searchbar.addEventListener("submit", event => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData($searchbar);
|
||||
const q = formData.get("q");
|
||||
let href = getUrl();
|
||||
let href = baseUrl();
|
||||
if (q) {
|
||||
href += "?q=" + q;
|
||||
}
|
||||
@@ -442,10 +482,8 @@ function setupSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup upload
|
||||
*/
|
||||
function setupUpload() {
|
||||
const $newFolder = document.querySelector(".new-folder");
|
||||
$newFolder.classList.remove("hidden");
|
||||
$newFolder.addEventListener("click", () => {
|
||||
const name = prompt("Enter folder name");
|
||||
@@ -460,19 +498,61 @@ function setupUpload() {
|
||||
});
|
||||
}
|
||||
|
||||
async function setupEditor() {
|
||||
document.querySelector(".editor-page").classList.remove("hidden");;
|
||||
|
||||
const $download = document.querySelector(".download")
|
||||
$download.classList.remove("hidden");
|
||||
$download.href = baseUrl()
|
||||
|
||||
if (!DATA.editable) {
|
||||
const $notEditable = document.querySelector(".not-editable");
|
||||
$notEditable.classList.remove("hidden");
|
||||
$notEditable.textContent = "File is binary or too large.";
|
||||
return;
|
||||
}
|
||||
|
||||
const $saveBtn = document.querySelector(".save-btn");
|
||||
$saveBtn.classList.remove("hidden");
|
||||
$saveBtn.addEventListener("click", saveChange);
|
||||
|
||||
$editor.classList.remove("hidden");
|
||||
try {
|
||||
const res = await fetch(baseUrl());
|
||||
await assertFetch(res);
|
||||
const text = await res.text();
|
||||
$editor.value = text;
|
||||
} catch (err) {
|
||||
alert(`Failed get file, ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save editor change
|
||||
*/
|
||||
async function saveChange() {
|
||||
try {
|
||||
await fetch(baseUrl(), {
|
||||
method: "PUT",
|
||||
body: $editor.value,
|
||||
});
|
||||
} catch (err) {
|
||||
alert(`Failed to save file, ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a folder
|
||||
* @param {string} name
|
||||
*/
|
||||
async function createFolder(name) {
|
||||
const url = getUrl(name);
|
||||
const url = newUrl(name);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "MKCOL",
|
||||
});
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
location.href = url;
|
||||
}
|
||||
await assertFetch(res);
|
||||
location.href = url;
|
||||
} catch (err) {
|
||||
alert(`Cannot create folder \`${name}\`, ${err.message}`);
|
||||
}
|
||||
@@ -492,15 +572,18 @@ async function addFileEntries(entries, dirs) {
|
||||
}
|
||||
|
||||
|
||||
function getUrl(name) {
|
||||
let url = location.href.split('?')[0];
|
||||
function newUrl(name) {
|
||||
let url = baseUrl();
|
||||
if (!url.endsWith("/")) url += "/";
|
||||
if (!name) return url;
|
||||
url += name.split("/").map(encodeURIComponent).join("/");
|
||||
return url;
|
||||
}
|
||||
|
||||
function getSvg(path_type) {
|
||||
function baseUrl() {
|
||||
return location.href.split('?')[0];
|
||||
}
|
||||
|
||||
function getPathSvg(path_type) {
|
||||
switch (path_type) {
|
||||
case "Dir":
|
||||
return `<svg height="16" viewBox="0 0 14 16" width="14"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"></path></svg>`;
|
||||
@@ -558,30 +641,8 @@ function encodedStr(rawStr) {
|
||||
});
|
||||
}
|
||||
|
||||
function ready() {
|
||||
document.title = `Index of ${DATA.href} - Dufs`;
|
||||
$pathsTable = document.querySelector(".paths-table")
|
||||
$pathsTableHead = document.querySelector(".paths-table thead");
|
||||
$pathsTableBody = document.querySelector(".paths-table tbody");
|
||||
$uploadersTable = document.querySelector(".uploaders-table");
|
||||
$emptyFolder = document.querySelector(".empty-folder");
|
||||
$newFolder = document.querySelector(".new-folder");
|
||||
$searchbar = document.querySelector(".searchbar");
|
||||
|
||||
if (DATA.allow_search) {
|
||||
setupSearch()
|
||||
}
|
||||
|
||||
if (DATA.allow_archive) {
|
||||
document.querySelector(".zip-root").classList.remove("hidden");
|
||||
}
|
||||
|
||||
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
||||
renderPathsTableHead();
|
||||
renderPathsTableBody();
|
||||
|
||||
if (DATA.allow_upload) {
|
||||
dropzone();
|
||||
setupUpload();
|
||||
async function assertFetch(res) {
|
||||
if (!(res.status >= 200 && res.status < 300)) {
|
||||
throw new Error(await res.text())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user