merge
This commit is contained in:
@ -12,7 +12,7 @@ export default class extends AbstractView {
|
||||
Akel is a game engine designed to be easy to use. The purpose of the project is learning about game engine development, discovering new rendering processes and learning to use new tools. It is mainly coded on and for Linux but is cross-platform and has been tested on Windows and MacOS.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://akel-engine.com" data-link>Akel Engine</a>.
|
||||
<a href="https://cdn.discordapp.com/attachments/1198285289236463699/1211450599007064074/malonerd.png?ex=6600b34a&is=65ee3e4a&hm=359877a4259663411dc24383562193e0e8862774022ca9989b6960f6628f1e2c&">Akel Engine</a>.
|
||||
<a href="/posts" data-link>View recent posts</a>.
|
||||
</p>
|
||||
`;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { client, navigateTo } from "../index.js";
|
||||
import { client, lang, navigateTo } from "../index.js";
|
||||
import { clear, fill_errors } from "../utils/formUtils.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
@ -7,56 +7,36 @@ export default class extends AbstractAuthenticatedView {
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Matchmaking");
|
||||
this.game_mode = 0; // 0 -> 2D; 1 -> 3D
|
||||
}
|
||||
|
||||
async press_button()
|
||||
async toggle_search()
|
||||
{
|
||||
clear("innerText", ["detail"]);
|
||||
if (client.matchmaking.searching)
|
||||
{
|
||||
client.matchmaking.stop();
|
||||
document.getElementById("button").value = "Find a game";
|
||||
this.button.innerHTML = lang.get("matchmakingStartSearch");
|
||||
}
|
||||
else
|
||||
{
|
||||
let nb_players = document.getElementById("nb_players-input").value;
|
||||
let nb_players = this.input.value;
|
||||
|
||||
await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), nb_players);
|
||||
|
||||
document.getElementById("button").value = "Stop matchmaking";
|
||||
this.button.innerHTML = lang.get("matchmakingStopSearch");
|
||||
}
|
||||
}
|
||||
|
||||
async press_button_game_mode()
|
||||
{
|
||||
if(this.game_mode === 0)
|
||||
{
|
||||
document.getElementById("game-mode").value = "3D";
|
||||
this.game_mode = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("game-mode").value = "2D";
|
||||
this.game_mode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ondisconnect(event)
|
||||
{
|
||||
let button = document.getElementById("button")
|
||||
|
||||
if (button === null)
|
||||
return
|
||||
|
||||
button.value = "Find a game";
|
||||
this.button.innerHTML = lang.get("matchmakingStartSearch");
|
||||
}
|
||||
|
||||
onreceive(data)
|
||||
{
|
||||
if (data.detail === "game_found")
|
||||
{
|
||||
navigateTo(`/games/${data.game_id}/${this.game_mode}`);
|
||||
navigateTo(`/games/${data.game_id}`);
|
||||
return;
|
||||
}
|
||||
this.display_data(data);
|
||||
@ -70,46 +50,44 @@ export default class extends AbstractAuthenticatedView {
|
||||
|
||||
async postInit()
|
||||
{
|
||||
let button = document.getElementById("button");
|
||||
this.button = document.getElementById("toggle-search");
|
||||
this.input = document.getElementById("nb-players-input");
|
||||
|
||||
button.onclick = this.press_button.bind(this);
|
||||
this.button.onclick = this.toggle_search.bind(this);
|
||||
|
||||
let input = document.getElementById("nb_players-input");
|
||||
this.input.addEventListener('keydown', async ev => {
|
||||
|
||||
input.addEventListener('keydown', async ev => {
|
||||
if (ev.key !== 'Enter')
|
||||
return;
|
||||
|
||||
if (client.matchmaking.searching)
|
||||
client.matchmaking.stop();
|
||||
|
||||
let nb_players = document.getElementById("nb_players-input").value;
|
||||
|
||||
await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), nb_players);
|
||||
|
||||
document.getElementById("button").value = "Stop matchmaking";
|
||||
await this.toggle_search.bind(this);
|
||||
});
|
||||
|
||||
let update = () => {
|
||||
if (input.value < 2 || input.value > 4)
|
||||
button.disabled = true;
|
||||
else
|
||||
button.disabled = false;
|
||||
this.button.disabled = (this.input.value < 2 || this.input.value > 4);
|
||||
};
|
||||
|
||||
["change", "oninput"].forEach((event_name) => {
|
||||
input.addEventListener(event_name, update);
|
||||
this.input.addEventListener(event_name, update);
|
||||
});
|
||||
document.getElementById("game-mode").onclick = this.press_button_game_mode.bind(this);
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return `
|
||||
<h1>Select mode</h1>
|
||||
<input type="number" value="2" min="1" max="4" id="nb_players-input">
|
||||
<input type="button" value="Find a game" id="button">
|
||||
<input type="button" value="2D" id="game-mode">
|
||||
<span id="detail"></span>
|
||||
return /* HTML */ `
|
||||
<div class='container-fluid'>
|
||||
<div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
|
||||
<h4 class='text-center fw-semibold mb-4' id="title">${lang.get("matchmakingTitle")}</h4>
|
||||
<div class='form-floating mb-2'>
|
||||
<input type='number' min='2' value='2' max='4' class='form-control' id='nb-players-input' placeholder='${lang.get("matchmakingNbPlayers")}'>
|
||||
<label for='nb-players-input' id='username-label'>${lang.get("matchmakingNbPlayers")}</label>
|
||||
<span class='text-danger' id='username'></span>
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='toggle-search'>${lang.get("matchmakingStartSearch")}</button>
|
||||
<span class='text-danger my-auto mx-2' id='detail'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -1,55 +1,82 @@
|
||||
import { client, navigateTo } from "../index.js";
|
||||
import { clear, fill_errors } from "../utils/formUtils.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
import { client, navigateTo } from '../index.js';
|
||||
import { clear, fill_errors } from '../utils/formUtils.js';
|
||||
import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js';
|
||||
|
||||
export default class extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Settings");
|
||||
super(params, 'Settings');
|
||||
this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
this.display_avatar();
|
||||
document.getElementById("save-account-button").onclick = () => this.save_account();
|
||||
document.getElementById("delete-account-button").onclick = () => this.delete_account();
|
||||
document.getElementById("save-profile-button").onclick = () => this.save_profile();
|
||||
this.avatarInit();
|
||||
this.usernameInit();
|
||||
|
||||
// document.getElementById('delete-account-button').onclick = () => this.delete_account();
|
||||
}
|
||||
|
||||
async display_avatar() {
|
||||
let profile = await client.profiles.getProfile(client.me.username);
|
||||
if (profile !== undefined || profile !== null) {
|
||||
if (document.getElementById("avatar") != undefined)
|
||||
document.getElementById("avatar").remove();
|
||||
let avatar = document.createElement("img");
|
||||
avatar.id = "avatar";
|
||||
avatar.src = profile.avatar_url + '?t=' +new Date().getTime();
|
||||
document.getElementsByClassName("avatar")[0].appendChild(avatar);
|
||||
usernameInit() {
|
||||
const usernameInput = document.getElementById('usernameInput');
|
||||
const usernameSave = document.getElementById('usernameSave');
|
||||
|
||||
usernameInput.oninput = e => {
|
||||
const value = e.target.value;
|
||||
if (value != client.me.username && value.length)
|
||||
usernameSave.classList.remove('disabled');
|
||||
else
|
||||
usernameSave.classList.add('disabled');
|
||||
}
|
||||
usernameSave.onclick = _ => this.saveUsername();
|
||||
}
|
||||
|
||||
avatarInit() {
|
||||
const avatar = document.getElementById('avatar');
|
||||
const avatarInput = document.getElementById('avatarInput');
|
||||
const avatarUpload = document.getElementById('avatarUpload');
|
||||
const avatarDelete = document.getElementById('avatarDelete');
|
||||
|
||||
avatar.onclick = _ => avatarInput.click();
|
||||
avatarInput.onchange = function () {
|
||||
const selectedFile = this.files[0];
|
||||
if (!selectedFile)
|
||||
return;
|
||||
|
||||
avatar.src = URL.createObjectURL(selectedFile);
|
||||
avatarUpload.classList.remove('d-none');
|
||||
}
|
||||
avatarUpload.onclick = _ => this.saveAvatar();
|
||||
avatarDelete.onclick = _ => this.deleteAvatar();
|
||||
}
|
||||
|
||||
async displayAvatar() {
|
||||
let avatar = document.getElementById('avatar');
|
||||
avatar.src = client.me.avatar_url + '?t=' + new Date().getTime();
|
||||
console.log(avatar.src);
|
||||
}
|
||||
|
||||
async delete_account()
|
||||
{
|
||||
let current_password = document.getElementById("current_password-input").value;
|
||||
let current_password = document.getElementById('current_password-input').value;
|
||||
|
||||
let response_data = await client.account.delete(current_password);
|
||||
|
||||
if (response_data === null || response_data === "user deleted")
|
||||
if (response_data === null || response_data === 'user deleted')
|
||||
{
|
||||
navigateTo("/login");
|
||||
navigateTo('/login');
|
||||
return;
|
||||
}
|
||||
clear("innerHTML", ["current_password-input"]);
|
||||
fill_errors({"current_password-input": response_data.password}, "innerHTML");
|
||||
clear('innerHTML', ['current_password-input']);
|
||||
fill_errors({'current_password-input': response_data.password}, 'innerHTML');
|
||||
}
|
||||
|
||||
async save_account()
|
||||
{
|
||||
let username = document.getElementById("username-input").value;
|
||||
let new_password = document.getElementById("new_password-input").value;
|
||||
let current_password = document.getElementById("current_password-input").value;
|
||||
let username = document.getElementById('username-input').value;
|
||||
let new_password = document.getElementById('new_password-input').value;
|
||||
let current_password = document.getElementById('current_password-input').value;
|
||||
|
||||
let data = {};
|
||||
|
||||
@ -61,67 +88,139 @@ export default class extends AbstractAuthenticatedView
|
||||
|
||||
if (response_data === null)
|
||||
{
|
||||
navigateTo("/login");
|
||||
navigateTo('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
if (response_data === "data has been alterate")
|
||||
response_data = {"save-account": "saved"};
|
||||
if (response_data === 'data has been alterate')
|
||||
response_data = {'save-account': 'saved'};
|
||||
|
||||
clear("innerHTML", ["username", "new_password", "current_password", "save-account", "delete-account"]);
|
||||
fill_errors(response_data, "innerHTML");
|
||||
}
|
||||
|
||||
async save_profile()
|
||||
{
|
||||
let avatar = document.getElementById("avatar-input");
|
||||
|
||||
if (avatar.files[0] !== undefined)
|
||||
{
|
||||
if (avatar.files[0].size > this.PROFILE_PICTURE_MAX_SIZE) {
|
||||
document.getElementById("save-profile").classList.add('text-danger');
|
||||
document.getElementById("save-profile").innerHTML = "Image too large :/";
|
||||
return;
|
||||
}
|
||||
let form_data = new FormData();
|
||||
form_data.append("avatar", avatar.files[0]);
|
||||
await client.me.change_avatar(form_data);
|
||||
this.display_avatar();
|
||||
}
|
||||
document.getElementById("save-profile").classList.remove('text-danger');
|
||||
document.getElementById("save-profile").innerHTML = "Saved";
|
||||
clear('innerHTML', ['username', 'new_password', 'current_password', 'save-account', 'delete-account']);
|
||||
fill_errors(response_data, 'innerHTML');
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return /* HTML */ `
|
||||
<link rel="stylesheet" href="/static/css/settings.css">
|
||||
<h1>ME</h1>
|
||||
<div id="main">
|
||||
<div class="avatar">
|
||||
</div>
|
||||
<div class="account">
|
||||
<h3>Account</h3>
|
||||
<input type="text" placeholder="username" id="username-input" text=${client.me.username}>
|
||||
<span id="username"></span>
|
||||
<input type=password placeholder="new_password" id="new_password-input">
|
||||
<span id="new_password"></span>
|
||||
<input type=password placeholder="current_password" id="current_password-input">
|
||||
<span id="current_password"></span>
|
||||
async saveUsername()
|
||||
{
|
||||
const usernameInput = document.getElementById('usernameInput');
|
||||
const username = usernameInput.value;
|
||||
const usernameDetail = document.getElementById('usernameDetail');
|
||||
|
||||
<input type="button" value="Save Credentials" id="save-account-button">
|
||||
<span id="save-account"></span>
|
||||
<input type="button" value="Delete Account" id="delete-account-button">
|
||||
<span id="delete-account"></span>
|
||||
</div>
|
||||
<div class="profile">
|
||||
<h3>Profile</h3>
|
||||
<input type="file" id="avatar-input" accept="image/*">
|
||||
<input type="button" value="Save profile" id="save-profile-button">
|
||||
<span id="save-profile"></span>
|
||||
</div>
|
||||
<a href="/logout" class="nav__link" data-link>Logout</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (!username.length || username === client.me.username)
|
||||
return;
|
||||
|
||||
const error = await client.account.updateUsername(username);
|
||||
if (!error) {
|
||||
usernameDetail.classList.remove('text-danger');
|
||||
usernameDetail.classList.add('text-success');
|
||||
usernameDetail.innerHTML = 'Username Saved.';
|
||||
setTimeout(_ => usernameDetail.innerHTML = '', 2000);
|
||||
document.getElementById('usernameSave').classList.add('disabled');
|
||||
} else {
|
||||
usernameDetail.classList.remove('text-success');
|
||||
usernameDetail.classList.add('text-danger');
|
||||
usernameDetail.innerHTML = error;
|
||||
document.getElementById('usernameSave').classList.add('disabled');
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async saveAvatar()
|
||||
{
|
||||
const avatarInput = document.getElementById('avatarInput');
|
||||
const selectedFile = avatarInput.files[0];
|
||||
const avatarDetail = document.getElementById('avatarDetail');
|
||||
|
||||
if (!selectedFile)
|
||||
return;
|
||||
|
||||
if (selectedFile.size > this.PROFILE_PICTURE_MAX_SIZE) {
|
||||
avatarDetail.classList.remove('text-success');
|
||||
avatarDetail.classList.add('text-danger');
|
||||
avatarDetail.innerHTML = 'Image is too large.'; //to translate
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await client.me.changeAvatar(selectedFile);
|
||||
if (!error) {
|
||||
avatarDetail.classList.remove('text-danger');
|
||||
avatarDetail.classList.add('text-success');
|
||||
avatarDetail.innerHTML = 'Avatar saved.'; //to translate
|
||||
setTimeout(_ => avatarDetail.innerHTML = '', 2000);
|
||||
document.getElementById('avatarDelete').classList.remove('d-none');
|
||||
document.getElementById('avatarUpload').classList.add('d-none');
|
||||
avatarInput.value = null;
|
||||
} else {
|
||||
avatarDetail.classList.remove('text-success');
|
||||
avatarDetail.classList.add('text-danger');
|
||||
avatarDetail.innerHTML = error.avatar[0];
|
||||
document.getElementById('avatarUpload').classList.add('d-none');
|
||||
avatarInput.value = null;
|
||||
console.log(error);
|
||||
}
|
||||
this.displayAvatar();
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
const avatarDetail = document.getElementById('avatarDetail');
|
||||
|
||||
const error = await client.me.deleteAvatar();
|
||||
if (!error) {
|
||||
avatarDetail.classList.remove('text-danger');
|
||||
avatarDetail.classList.add('text-success');
|
||||
avatarDetail.innerHTML = 'Avatar deleted.'; //to translate
|
||||
setTimeout(_ => avatarDetail.innerHTML = '', 2000);
|
||||
document.getElementById('avatarDelete').classList.add('d-none');
|
||||
} else {
|
||||
avatarDetail.classList.remove('text-success');
|
||||
avatarDetail.classList.add('text-danger');
|
||||
avatarDetail.innerHTML = 'Something went wrong.'; //to translate
|
||||
}
|
||||
this.displayAvatar();
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
const avatarUnchanged = client.me.avatar_url === '/static/avatars/default.avif';
|
||||
|
||||
return /* HTML */ `
|
||||
<div class='container col-sm-10 col-8 d-flex rounded border border-2 bg-light-subtle py-3'>
|
||||
<div class='row col-4 bg-body-tertiary border rounded p-2 m-2 d-flex justify-content-center align-items-center'>
|
||||
<h2 class='border-bottom'>Avatar</h2>
|
||||
<img id='avatar' class='rounded p-0' src=${client.me.avatar_url} style='cursor: pointer;'>
|
||||
<input id='avatarInput' class='d-none' type='file' accept='image/*'>
|
||||
<div class='d-flex gap-2 mt-1 px-0'>
|
||||
<span class='my-auto ms-1 me-auto' id='avatarDetail'></span>
|
||||
<button class='btn btn-primary d-none' id='avatarUpload'>Save</button>
|
||||
<button class='btn btn-danger${avatarUnchanged ? ' d-none' : ''}' id='avatarDelete'>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h1 class='border-bottom ps-1 mb-3'>Account</h1>
|
||||
<div>
|
||||
<div class='input-group'>
|
||||
<div class='form-floating'>
|
||||
<input type='text' class='form-control' id='usernameInput' placeholder='username' value=${client.me.username}>
|
||||
<label for='usernameInput'>Username</label>
|
||||
</div>
|
||||
<button class='input-group-text btn btn-success disabled' id='usernameSave'>Save</button>
|
||||
</div>
|
||||
<span class='form-text' id='usernameDetail'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// <input class='form-control' type='text' placeholder='Username' id='username-input' value=${client.me.username}>
|
||||
// <h1>Settings</h1>
|
||||
// <input class='form-control d-inline-block' type='text' placeholder='Username' id='username-input' value=${client.me.username}>
|
||||
// <span id='username'></span>
|
||||
// <input type=password placeholder='New Password' id='new_password-input'>
|
||||
// <span id='new_password'></span>
|
||||
// <input type=password placeholder='Current Password' id='current_password-input'>
|
||||
// <span id='current_password'></span>
|
||||
// <input type='button' value='Save Credentials' id='save-account-button'>
|
||||
// <span id='save-account'></span>
|
||||
//
|
||||
// <input type='button' value='Delete Account' id='delete-account-button'>
|
||||
// <span id='delete-account'></span>
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {client, navigateTo} from "../../index.js";
|
||||
import {client, lang, navigateTo} from "../../index.js";
|
||||
import { clear, fill_errors } from "../../utils/formUtils.js";
|
||||
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
@ -13,13 +13,11 @@ export default class extends AbstractAuthenticatedView
|
||||
async create()
|
||||
{
|
||||
let name = document.getElementById("name-input").value;
|
||||
let nb_players = document.getElementById("nb_players-input").value;
|
||||
let nb_players_by_game = document.getElementById("nb_players_by_game-input").value;
|
||||
let nb_players = document.getElementById("nb-players-input").value;
|
||||
let nb_players_by_game = document.getElementById("nb-players-by-game-input").value;
|
||||
|
||||
let response_data = await client.tournaments.createTournament(nb_players, nb_players_by_game, name);
|
||||
|
||||
if (response_data === null)
|
||||
return;
|
||||
let response = await client.tournaments.createTournament(nb_players, nb_players_by_game, name);
|
||||
let response_data = await response.json();
|
||||
|
||||
let id = response_data.id;
|
||||
if (id !== undefined)
|
||||
@ -37,16 +35,33 @@ export default class extends AbstractAuthenticatedView
|
||||
document.getElementById("create-button").onclick = this.create;
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return `
|
||||
<input type="text" id="name-input" placeholder="Tournament name">
|
||||
<span id="name"></span>
|
||||
<input type="number" id="nb_players-input" placeholder="Number of players in tournament">
|
||||
<span id="nb_players"></span>
|
||||
<input type="number" id="nb_players_by_game-input" placeholder="Number of players by game">
|
||||
<span id="nb_players_by_game"></span>
|
||||
<input type="button" id="create-button" value="Create tournament">
|
||||
async getHtml() {
|
||||
|
||||
return /* HTML */ `
|
||||
<div class='container-fluid'>
|
||||
<div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
|
||||
<h4 class='text-center fw-semibold mb-4' id="title">${lang.get("TournamentCreateTitle")}</h4>
|
||||
<div class='form-floating mb-2'>
|
||||
<input type='text' class='form-control' id='name-input' placeholder='${lang.get("TournamentCreateTournamentName")}'>
|
||||
<label for='name-input' id='name-label'>${lang.get("TournamentCreateTournamentName")}</label>
|
||||
<span class='text-danger' id='name'></span>
|
||||
</div>
|
||||
<div class='form-floating mb-2'>
|
||||
<input type='number' class='form-control' min='2' max='4' value='2' id='nb-players-by-game-input' placeholder='${lang.get("TournamentCreateNbPlayerByGame")}'>
|
||||
<label for='nb-players-by-game-input' id='nb-players-by-game-label'>${lang.get("TournamentCreateNbPlayerByGame")}</label>
|
||||
<span class='text-danger' id='nb_players_by_game'></span>
|
||||
</div>
|
||||
<div class='form-floating mb-2'>
|
||||
<input type='number' class='form-control' min='2' value='4' id='nb-players-input' placeholder='${lang.get("TournamentCreateNbPlayer")}'>
|
||||
<label for='nb-players-input' id='nb-players-label'>${lang.get("TournamentCreateNbPlayer")}</label>
|
||||
<span class='text-danger' id='nb_players'></span>
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='create-button'>${lang.get("TournamentCreateButton")}</button>
|
||||
<span class='text-danger my-auto mx-2' id='detail'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export default class extends AbstractAuthenticatedView
|
||||
document.getElementById("level").innerText = this.tournament.level;
|
||||
document.getElementById("state").innerText = this.tournament.state;
|
||||
|
||||
if (this.tournament.state === "waiting")
|
||||
if (this.tournament.started === false)
|
||||
button.disabled = false;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user