mirror of
https://github.com/Bismuth-Forge/bismuth.git
synced 2024-09-17 11:37:10 +03:00
convert to typescript
This commit is contained in:
parent
77f3d34799
commit
4f455d8e46
@ -18,63 +18,83 @@
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
function KWinDriver() {
|
||||
var self = this;
|
||||
var engine = null;
|
||||
class KWinDriver
|
||||
{
|
||||
engine: TilingEngine;
|
||||
|
||||
/*
|
||||
* Signal handlers
|
||||
*/
|
||||
|
||||
self._onClientAdded = function(client) {
|
||||
// DEBUG: print("clientAdded " + client + " " + client.resourceClass);
|
||||
private bindEvents()
|
||||
{
|
||||
workspace.clientAdded.connect(this.onClientAdded);
|
||||
workspace.clientRemoved.connect(this.onClientRemoved);
|
||||
workspace.numberScreensChanged.connect(this.onNumberScreensChanged);
|
||||
|
||||
var engine_arrange = () => { this.engine.arrange(); }
|
||||
workspace.clientMinimized.connect(engine_arrange);
|
||||
workspace.clientUnminimized.connect(engine_arrange);
|
||||
workspace.currentDesktopChanged.connect(engine_arrange);
|
||||
|
||||
// TODO: store screen size in engine?
|
||||
workspace.screenResized.connect(engine_arrange);
|
||||
|
||||
// TODO: handle workspace.clientMaximizeSet signal
|
||||
// TODO: handle workspace.clientFullScreenSet signal
|
||||
// TODO: handle workspace.currentActivityChanged signal
|
||||
// TODO: handle workspace.activitiesChanged signal
|
||||
// TODO: handle workspace.activityAdded signal
|
||||
// TODO: handle workspace.activityRemoved signal
|
||||
// TODO: handle workspace.numberDesktopsChanged signal(???)
|
||||
}
|
||||
|
||||
private onClientAdded = (client: KWin.Client) =>
|
||||
{
|
||||
// TODO: check resourceClasses for some windows
|
||||
if(!engine.clientManage(client))
|
||||
return;
|
||||
this.engine.manageClient(client);
|
||||
|
||||
client.desktopChanged.connect(engine.arrange);
|
||||
client.geometryChanged.connect(function() {
|
||||
client.desktopChanged.connect(this.engine.arrange);
|
||||
client.geometryChanged.connect(() => {
|
||||
if(client.move || client.resize) return;
|
||||
engine.clientArrange(client);
|
||||
this.engine.arrangeClient(client);
|
||||
});
|
||||
client.moveResizedChanged.connect(function() {
|
||||
client.moveResizedChanged.connect(() => {
|
||||
if(client.move || client.resize) return;
|
||||
engine.arrange();
|
||||
this.engine.arrange();
|
||||
});
|
||||
|
||||
// DEBUG: print(" -> numTiles=" + engine.tiles.length);
|
||||
};
|
||||
|
||||
self._onClientRemoved = function(client) {
|
||||
// DEBUG: print("clientRemoved " + client);
|
||||
private onClientRemoved = (client: KWin.Client) =>
|
||||
{
|
||||
/* XXX: This is merely an attempt to remove the exited client.
|
||||
* Sometimes, the client is not found in the tile list, and causes an
|
||||
* exception in `engine.arrange`.
|
||||
*/
|
||||
engine.clientUnmanage(client);
|
||||
// DEBUG: print(" -> numTiles=" + engine.tiles.length);
|
||||
this.engine.unmanageClient(client);
|
||||
};
|
||||
|
||||
self._onNumberScreensChanged = function(count) {
|
||||
// DEBUG: print("numberScreenChanged " + count);
|
||||
while(engine.screens.length < count)
|
||||
engine.screenAdd(engine.screens.length);
|
||||
while(engine.screens.length > count)
|
||||
engine.screenRemove(engine.screens.length - 1);
|
||||
private onNumberScreensChanged = (count: number) =>
|
||||
{
|
||||
while(this.engine.screens.length < count)
|
||||
this.engine.addScreen(this.engine.screens.length);
|
||||
while(this.engine.screens.length > count)
|
||||
this.engine.screenRemove(this.engine.screens.length - 1);
|
||||
};
|
||||
|
||||
/*
|
||||
* Utils
|
||||
*/
|
||||
|
||||
self.getWorkingArea = function(screenId) {
|
||||
public getWorkingArea(screenId: number): QRect
|
||||
{
|
||||
// TODO: verify: can each desktops have a different placement area?
|
||||
return workspace.clientArea(
|
||||
KWin.PlacementArea, screenId, workspace.currentDesktop);
|
||||
}
|
||||
|
||||
self.getClientGeometry = function(client) {
|
||||
public getClientGeometry(client: KWin.Client): QRect
|
||||
{
|
||||
return {
|
||||
x: client.geometry.x,
|
||||
y: client.geometry.y,
|
||||
@ -83,11 +103,18 @@ function KWinDriver() {
|
||||
};
|
||||
}
|
||||
|
||||
self.setClientGeometry = function(client, x, y, width, height) {
|
||||
client.geometry = { x:x, y:y, width:width, height:height };
|
||||
public setClientGeometry(client: KWin.Client, geometry: QRect)
|
||||
{
|
||||
client.geometry = {
|
||||
x: geometry.x,
|
||||
y: geometry.y,
|
||||
width: geometry.width,
|
||||
height: geometry.height
|
||||
};
|
||||
}
|
||||
|
||||
self.isClientVisible = function(client, screenId) {
|
||||
public isClientVisible(client: KWin.Client, screenId: number)
|
||||
{
|
||||
// TODO: test KWin::Toplevel properties...?
|
||||
return (
|
||||
(!client.minimized) &&
|
||||
@ -101,30 +128,13 @@ function KWinDriver() {
|
||||
* Main
|
||||
*/
|
||||
|
||||
self.main = function() {
|
||||
engine = new TilingEngine(self);
|
||||
public main()
|
||||
{
|
||||
this.engine = new TilingEngine(this);
|
||||
|
||||
workspace.clientAdded.connect(self._onClientAdded);
|
||||
workspace.clientRemoved.connect(self._onClientRemoved);
|
||||
workspace.numberScreensChanged.connect(self._onNumberScreensChanged);
|
||||
|
||||
workspace.clientMinimized.connect(engine.arrange);
|
||||
workspace.clientUnminimized.connect(engine.arrange);
|
||||
workspace.currentDesktopChanged.connect(engine.arrange);
|
||||
|
||||
// TODO: store screen size in engine?
|
||||
workspace.screenResized.connect(engine.arrange);
|
||||
|
||||
// TODO: handle workspace.clientMaximizeSet signal
|
||||
// TODO: handle workspace.clientFullScreenSet signal
|
||||
// TODO: handle workspace.currentActivityChanged signal
|
||||
// TODO: handle workspace.activitiesChanged signal
|
||||
// TODO: handle workspace.activityAdded signal
|
||||
// TODO: handle workspace.activityRemoved signal
|
||||
// TODO: handle workspace.numberDesktopsChanged signal(???)
|
||||
|
||||
self._onNumberScreensChanged(workspace.numScreens);
|
||||
workspace.clientList().map(self._onClientAdded);
|
||||
this.bindEvents();
|
||||
this.onNumberScreensChanged(workspace.numScreens);
|
||||
workspace.clientList().map(this.onClientAdded);
|
||||
}
|
||||
}
|
||||
|
157
src/engine.js
157
src/engine.js
@ -1,157 +0,0 @@
|
||||
// Copyright (c) 2018 Eon S. Jeon <esjeon@hyunmu.am>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
function Screen(id) {
|
||||
this.id = id;
|
||||
this.layout = layout_tile; //null;
|
||||
this.layoutOpts = {}
|
||||
}
|
||||
|
||||
function Tile(client) {
|
||||
this.client = client;
|
||||
this.isNew = true;
|
||||
this.isError = false;
|
||||
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
}
|
||||
|
||||
// TODO: declare Layout class (`layout.js`?)
|
||||
// TODO: layouts in separate file(s)
|
||||
function layout_tile(tiles, areaWidth, areaHeight, opts) {
|
||||
if(!opts.tile_ratio) opts.tile_ratio = 0.45;
|
||||
if(!opts.tile_nmaster) opts.tile_nmaster = 1;
|
||||
|
||||
var masterCount, masterWidth, masterHeight;
|
||||
var stackCount , stackWidth, stackHeight, stackX;
|
||||
|
||||
if(tiles.length <= opts.tile_nmaster) {
|
||||
masterCount = tiles.length;
|
||||
masterWidth = areaWidth;
|
||||
masterHeight = Math.floor(areaHeight / masterCount);
|
||||
|
||||
stackCount = stackWidth = stackHeight = stackX = 0;
|
||||
} else {
|
||||
masterCount = opts.tile_nmaster;
|
||||
masterWidth = Math.floor(areaWidth * (1 - opts.tile_ratio));
|
||||
masterHeight = Math.floor(areaHeight / masterCount);
|
||||
|
||||
stackCount = tiles.length - masterCount;
|
||||
stackWidth = areaWidth - masterWidth;
|
||||
stackHeight = Math.floor(areaHeight / stackCount);
|
||||
stackX = masterWidth + 1;
|
||||
}
|
||||
|
||||
for(var i = 0; i < masterCount; i++) {
|
||||
tiles[i].x = 0;
|
||||
tiles[i].y = masterHeight * i;
|
||||
tiles[i].width = masterWidth;
|
||||
tiles[i].height = masterHeight;
|
||||
}
|
||||
|
||||
for(var i = 0; i < stackCount; i++) {
|
||||
var j = masterCount + i;
|
||||
tiles[j].x = stackX;
|
||||
tiles[j].y = stackHeight * i;
|
||||
tiles[j].width = stackWidth;
|
||||
tiles[j].height = stackHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function TilingEngine(driver) {
|
||||
var self = this;
|
||||
|
||||
self.tiles = Array();
|
||||
self.screens = Array();
|
||||
|
||||
self.arrange = function() {
|
||||
self.screens.forEach(function(screen) {
|
||||
if(screen.layout === null)
|
||||
return;
|
||||
|
||||
var area = driver.getWorkingArea(screen.id);
|
||||
var visibles = self.tiles
|
||||
.filter(function(t) {
|
||||
try {
|
||||
return driver.isClientVisible(t.client, screen.id);
|
||||
} catch(e) {
|
||||
t.isError = true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// TODO: fullscreen handling
|
||||
screen.layout(visibles, area.width, area.height, screen.layoutOpts);
|
||||
|
||||
visibles.forEach(function(tile) {
|
||||
driver.setClientGeometry(
|
||||
tile.client, tile.x, tile.y, tile.width, tile.height);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
self.clientArrange = function(client) {
|
||||
self.tiles.forEach(function(tile) {
|
||||
if(tile.client != client) return;
|
||||
|
||||
var geometry = driver.getClientGeometry(tile.client);
|
||||
if(geometry.x == tile.x)
|
||||
if(geometry.y == tile.y)
|
||||
if(geometry.width == tile.width)
|
||||
if(geometry.height == tile.height)
|
||||
return;
|
||||
|
||||
driver.setClientGeometry(
|
||||
tile.client, tile.x, tile.y, tile.width, tile.height);
|
||||
});
|
||||
}
|
||||
|
||||
self.clientManage = function(client) {
|
||||
if(client.specialWindow)
|
||||
return false;
|
||||
|
||||
self.tiles.push(new Tile(client));
|
||||
self.arrange();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clientUnmanage = function(client) {
|
||||
self.tiles = self.tiles
|
||||
.filter(function(t) {
|
||||
return t.client != client && !t.isError;
|
||||
});
|
||||
self.arrange();
|
||||
}
|
||||
|
||||
self.screenAdd = function(screenId) {
|
||||
self.screens.push(new Screen(screenId));
|
||||
}
|
||||
|
||||
self.screenRemove = function(screenId) {
|
||||
self.screens = self.screens
|
||||
.filter(function(screen) {
|
||||
return screen.id !== screenId;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
177
src/engine.ts
Normal file
177
src/engine.ts
Normal file
@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2018 Eon S. Jeon <esjeon@hyunmu.am>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class Screen
|
||||
{
|
||||
id: number;
|
||||
layout: any;
|
||||
layoutOpts: any;
|
||||
|
||||
constructor(id: number)
|
||||
{
|
||||
this.id = id;
|
||||
this.layout = layout_tile;
|
||||
this.layoutOpts = {};
|
||||
}
|
||||
}
|
||||
|
||||
class Tile
|
||||
{
|
||||
client: KWin.Client;
|
||||
isNew: boolean;
|
||||
isError: boolean;
|
||||
|
||||
geometry: QRect;
|
||||
|
||||
constructor(client: KWin.Client)
|
||||
{
|
||||
this.client = client;
|
||||
this.isNew = true;
|
||||
this.isError = false;
|
||||
|
||||
this.geometry = {
|
||||
x: 0, y: 0,
|
||||
width: 0, height: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: declare Layout class (`layout.js`?)
|
||||
// TODO: layouts in separate file(s)
|
||||
function layout_tile(tiles: Tile[], areaWidth: number, areaHeight: number, opts: any) {
|
||||
if(!opts.tile_ratio) opts.tile_ratio = 0.45;
|
||||
if(!opts.tile_nmaster) opts.tile_nmaster = 1;
|
||||
|
||||
var masterCount, masterWidth, masterHeight;
|
||||
var stackCount , stackWidth, stackHeight, stackX;
|
||||
|
||||
if(tiles.length <= opts.tile_nmaster) {
|
||||
masterCount = tiles.length;
|
||||
masterWidth = areaWidth;
|
||||
masterHeight = Math.floor(areaHeight / masterCount);
|
||||
|
||||
stackCount = stackWidth = stackHeight = stackX = 0;
|
||||
} else {
|
||||
masterCount = opts.tile_nmaster;
|
||||
masterWidth = Math.floor(areaWidth * (1 - opts.tile_ratio));
|
||||
masterHeight = Math.floor(areaHeight / masterCount);
|
||||
|
||||
stackCount = tiles.length - masterCount;
|
||||
stackWidth = areaWidth - masterWidth;
|
||||
stackHeight = Math.floor(areaHeight / stackCount);
|
||||
stackX = masterWidth + 1;
|
||||
}
|
||||
|
||||
for(var i = 0; i < masterCount; i++) {
|
||||
tiles[i].geometry.x = 0;
|
||||
tiles[i].geometry.y = masterHeight * i;
|
||||
tiles[i].geometry.width = masterWidth;
|
||||
tiles[i].geometry.height = masterHeight;
|
||||
}
|
||||
|
||||
for(var i = 0; i < stackCount; i++) {
|
||||
var j = masterCount + i;
|
||||
tiles[j].geometry.x = stackX;
|
||||
tiles[j].geometry.y = stackHeight * i;
|
||||
tiles[j].geometry.width = stackWidth;
|
||||
tiles[j].geometry.height = stackHeight;
|
||||
}
|
||||
}
|
||||
|
||||
class TilingEngine
|
||||
{
|
||||
driver: KWinDriver;
|
||||
tiles: Tile[];
|
||||
screens: Screen[];
|
||||
|
||||
constructor(driver: KWinDriver)
|
||||
{
|
||||
this.driver = driver;
|
||||
this.tiles = Array();
|
||||
this.screens = Array();
|
||||
}
|
||||
|
||||
public arrange = () =>
|
||||
{
|
||||
this.screens.forEach((screen) => {
|
||||
if(screen.layout === null)
|
||||
return;
|
||||
|
||||
var area = this.driver.getWorkingArea(screen.id);
|
||||
var visibles = this.tiles.filter((t) => {
|
||||
try {
|
||||
return this.driver.isClientVisible(t.client, screen.id);
|
||||
} catch(e) {
|
||||
t.isError = true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// TODO: fullscreen handling
|
||||
screen.layout(visibles, area.width, area.height, screen.layoutOpts);
|
||||
|
||||
visibles.forEach((tile) => {
|
||||
this.driver.setClientGeometry(tile.client, tile.geometry);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public arrangeClient = (client: KWin.Client) => {
|
||||
this.tiles.forEach((tile) => {
|
||||
if(tile.client != client) return;
|
||||
|
||||
var geometry = this.driver.getClientGeometry(tile.client);
|
||||
if(geometry.x == tile.geometry.x)
|
||||
if(geometry.y == tile.geometry.y)
|
||||
if(geometry.width == tile.geometry.width)
|
||||
if(geometry.height == tile.geometry.height)
|
||||
return;
|
||||
|
||||
this.driver.setClientGeometry(tile.client, tile.geometry);
|
||||
});
|
||||
}
|
||||
|
||||
public manageClient = (client: KWin.Client) => {
|
||||
// TODO: move this to KWinDriver
|
||||
if(client.specialWindow)
|
||||
return false;
|
||||
|
||||
this.tiles.push(new Tile(client));
|
||||
this.arrange();
|
||||
return true;
|
||||
}
|
||||
|
||||
public unmanageClient = (client: KWin.Client) => {
|
||||
this.tiles = this.tiles.filter(function(t) {
|
||||
return t.client != client && !t.isError;
|
||||
});
|
||||
this.arrange();
|
||||
}
|
||||
|
||||
public addScreen = (screenId: number) => {
|
||||
this.screens.push(new Screen(screenId));
|
||||
}
|
||||
|
||||
public screenRemove = (screenId: number) => {
|
||||
this.screens = this.screens.filter(function(screen) {
|
||||
return screen.id !== screenId;
|
||||
});
|
||||
}
|
||||
}
|
89
src/kwin.d.ts
vendored
Normal file
89
src/kwin.d.ts
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
// API Reference:
|
||||
// https://techbase.kde.org/Development/Tutorials/KWin/Scripting/API_4.9
|
||||
|
||||
interface QRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface QSignal {
|
||||
connect(callback: any): void;
|
||||
}
|
||||
|
||||
declare namespace KWin {
|
||||
/* enum ClientAreaOption */
|
||||
var PlacementArea: number;
|
||||
|
||||
function registerShortcut(
|
||||
title: string,
|
||||
text: string,
|
||||
keySequence: string,
|
||||
callback: any
|
||||
): boolean;
|
||||
|
||||
interface WorkspaceWrapper {
|
||||
/* read-only */
|
||||
readonly numScreens: number;
|
||||
|
||||
/* read-write */
|
||||
currentDesktop: number;
|
||||
|
||||
/* signals */
|
||||
clientAdded: QSignal;
|
||||
clientRemoved: QSignal;
|
||||
numberScreensChanged: QSignal;
|
||||
clientMinimized: QSignal;
|
||||
clientUnminimized: QSignal;
|
||||
currentDesktopChanged: QSignal;
|
||||
screenResized: QSignal;
|
||||
clientMaximizeSet: QSignal;
|
||||
clientFullScreenSet: QSignal;
|
||||
currentActivityChanged: QSignal;
|
||||
activitiesChanged: QSignal;
|
||||
activityAdded: QSignal;
|
||||
activityRemoved: QSignal;
|
||||
numberDesktopsChanged: QSignal;
|
||||
|
||||
/* functions */
|
||||
clientList(): Client[];
|
||||
clientArea(option: number, screen: number, desktop: number);
|
||||
}
|
||||
|
||||
interface Toplevel {
|
||||
/* read-only */
|
||||
readonly screen: number;
|
||||
readonly resourceName: string;
|
||||
readonly resourceClass: string;
|
||||
readonly windowRole: string;
|
||||
|
||||
/* signal */
|
||||
geometryChanged: QSignal;
|
||||
}
|
||||
|
||||
interface Client extends Toplevel {
|
||||
/* read-only */
|
||||
readonly caption: string;
|
||||
readonly move: boolean;
|
||||
readonly resize: boolean;
|
||||
readonly specialWindow: boolean;
|
||||
|
||||
/* read-write */
|
||||
desktop: number;
|
||||
onAllDesktops: boolean;
|
||||
fullScreen: boolean;
|
||||
geometry: QRect;
|
||||
keepAbove: boolean;
|
||||
keepBelow: boolean;
|
||||
minimized: boolean;
|
||||
noBorder: boolean;
|
||||
|
||||
/* signals */
|
||||
desktopChanged: QSignal;
|
||||
moveResizedChanged: QSignal;
|
||||
}
|
||||
}
|
||||
|
||||
declare var workspace: KWin.WorkspaceWrapper;
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"outFile": "script.js",
|
||||
"removeComments": true,
|
||||
"lib": ["es5"],
|
||||
"alwaysStrict": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user