mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #2132 from Tyriar/ts_wp
Enable absolute imports and minify distributable
This commit is contained in:
+1
-4
@@ -2,6 +2,7 @@ node_modules/
|
||||
*.swp
|
||||
.lock-wscript
|
||||
lib/
|
||||
out/
|
||||
Makefile.gyp
|
||||
*.Makefile
|
||||
*.target.gyp.mk
|
||||
@@ -16,10 +17,6 @@ build/
|
||||
fixtures/typings-test/*.js
|
||||
package-lock.json
|
||||
|
||||
# Directories needed for code coverage
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
demo/dist/
|
||||
|
||||
@@ -45,5 +45,4 @@ docs/
|
||||
bin/
|
||||
build/
|
||||
fixtures/
|
||||
coverage/
|
||||
demo/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||
|
||||
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
|
||||
|
||||
|
||||
+4
-9
@@ -20,7 +20,7 @@ jobs:
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
yarn test-unit
|
||||
displayName: 'Unit tests'
|
||||
- script: |
|
||||
yarn lint
|
||||
@@ -38,16 +38,11 @@ jobs:
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
yarn test-unit
|
||||
displayName: 'Unit tests'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
- script: |
|
||||
yarn test-coverage
|
||||
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCHNAME
|
||||
yarn coveralls
|
||||
displayName: 'Generate and publish coverage'
|
||||
|
||||
- job: Windows
|
||||
pool:
|
||||
@@ -61,12 +56,12 @@ jobs:
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
yarn test-unit
|
||||
displayName: 'Unit tests'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
|
||||
|
||||
- job: IntegrationTests
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
// Add `out` to the NODE_PATH so absolute paths can be resolved.
|
||||
const env = { ...process.env };
|
||||
env.NODE_PATH = path.resolve(__dirname, '../out');
|
||||
|
||||
const args = [
|
||||
'./out/*test.js',
|
||||
'./out/**/*test.js',
|
||||
'./out/*integration.js',
|
||||
'./out/**/*integration.js',
|
||||
'./lib/**/*test.js'
|
||||
];
|
||||
|
||||
cp.spawnSync(path.resolve(__dirname, '../node_modules/.bin/mocha'), args, {
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
env,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
+5
-1
@@ -7,7 +7,11 @@
|
||||
|
||||
/// <reference path="../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from '../lib/public/Terminal';
|
||||
// Use tsc version (yarn watch)
|
||||
import { Terminal } from '../out/public/Terminal';
|
||||
// Use webpacked version (yarn package)
|
||||
// import { Terminal } from '../lib/xterm';
|
||||
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/logo.png">
|
||||
<link rel="stylesheet" href="/src/xterm.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="/xterm.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
</head>
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ function startServer() {
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
app.use('/src', express.static(__dirname + '/../src'));
|
||||
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
|
||||
app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png'));
|
||||
|
||||
app.get('/', function(req, res){
|
||||
|
||||
+19
-2
@@ -11,7 +11,19 @@ const startServer = require('./server.js');
|
||||
|
||||
startServer();
|
||||
|
||||
// Build/watch client source
|
||||
/**
|
||||
* This webpack config builds and watches the demo project. It works by taking the output from tsc
|
||||
* (via `yarn watch`) which is put into `out/` and then webpacks it into `demo/dist/`. The aliases
|
||||
* are used fix up the absolute paths output by tsc (because of `baseUrl` and `paths` in
|
||||
* `tsconfig.json`.
|
||||
*
|
||||
* For production builds see `webpack.config.js` in the root directory. If that is built the demo
|
||||
* can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for
|
||||
* validating that the packaged version works correctly.
|
||||
*
|
||||
* The addons are not webpacked right now and are built directly to `lib/` via `tsc` as they are
|
||||
* the legacy format (`applyAddon`) and will be removed soon anyway.
|
||||
*/
|
||||
const clientConfig = {
|
||||
entry: path.resolve(__dirname, 'client.ts'),
|
||||
devtool: 'inline-source-map',
|
||||
@@ -32,7 +44,12 @@ const clientConfig = {
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, '..'), 'node_modules'],
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
extensions: [ '.tsx', '.ts', '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('./out/common'),
|
||||
core: path.resolve('./out/core'),
|
||||
ui: path.resolve('./out/ui')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: 'client-bundle.js',
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const browserify = require('browserify');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const fs = require('fs-extra');
|
||||
const gulp = require('gulp');
|
||||
const path = require('path');
|
||||
const merge = require('merge-stream');
|
||||
const mocha = require('gulp-mocha');
|
||||
const sorcery = require('sorcery');
|
||||
const source = require('vinyl-source-stream');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const ts = require('gulp-typescript');
|
||||
const util = require('gulp-util');
|
||||
|
||||
const buildDir = process.env.BUILD_DIR || 'build';
|
||||
const tsProject = ts.createProject('src/tsconfig.json');
|
||||
let srcDir = './src';
|
||||
let outDir = './lib';
|
||||
|
||||
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
||||
|
||||
const TEST_PATHS = [
|
||||
`${outDir}/*test.js`,
|
||||
`${outDir}/**/*test.js`,
|
||||
`${outDir}/*integration.js`,
|
||||
`${outDir}/**/*integration.js`
|
||||
];
|
||||
|
||||
// Under some environments like TravisCI, this comes out at absolute which can
|
||||
// break the build. This ensures that the outDir is absolute.
|
||||
if (path.normalize(outDir).indexOf(__dirname) !== 0) {
|
||||
outDir = `${__dirname}/${path.normalize(outDir)}`;
|
||||
}
|
||||
|
||||
gulp.task('css', function() {
|
||||
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
|
||||
});
|
||||
|
||||
gulp.task('watch-css', function() {
|
||||
return gulp.watch(`${srcDir}/**/*.css`, ['css']);
|
||||
});
|
||||
|
||||
/**
|
||||
* Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with
|
||||
* Browserify.
|
||||
*/
|
||||
gulp.task('browserify', function() {
|
||||
// Ensure that the build directory exists
|
||||
fs.ensureDirSync(buildDir);
|
||||
|
||||
let browserifyOptions = {
|
||||
basedir: buildDir,
|
||||
debug: true,
|
||||
entries: [`${outDir}/xterm.js`],
|
||||
standalone: 'Terminal',
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
let bundleStream = browserify(browserifyOptions)
|
||||
.bundle()
|
||||
.pipe(source(`xterm.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
|
||||
// Copy stylesheets from ${outDir}/ to ${buildDir}/
|
||||
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir));
|
||||
|
||||
return merge(bundleStream, copyStylesheets);
|
||||
});
|
||||
|
||||
gulp.task('browserify-addons', function() {
|
||||
const bundles = addons.map((addon) => {
|
||||
const addonOptions = {
|
||||
basedir: `${buildDir}/addons/${addon}`,
|
||||
debug: true,
|
||||
entries: [`${outDir}/addons/${addon}/${addon}.js`],
|
||||
standalone: addon,
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
|
||||
const addonBundle = browserify(addonOptions)
|
||||
.external(path.join(outDir, 'Terminal.js'))
|
||||
.bundle()
|
||||
.pipe(source(`./addons/${addon}/${addon}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
|
||||
return addonBundle;
|
||||
});
|
||||
|
||||
return merge(...bundles);
|
||||
});
|
||||
|
||||
gulp.task('mocha', function () {
|
||||
return gulp.src(TEST_PATHS, {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', () => process.exit(1));
|
||||
});
|
||||
|
||||
/**
|
||||
* Run single test suite (file) by file name (without file extension). Example of the command:
|
||||
* gulp mocha-suite --test InputHandler.test
|
||||
*/
|
||||
gulp.task('mocha-suite', [], function () {
|
||||
let testName = util.env.test;
|
||||
util.log("Run test by Name: " + testName);
|
||||
return gulp.src([`${outDir}/${testName}.js`, `${outDir}/**/${testName}.js`], {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', () => process.exit(1));
|
||||
});
|
||||
|
||||
/**
|
||||
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
|
||||
* (Without this task the source maps produced for the JavaScript bundle points into the
|
||||
* compiled JavaScript files in ${outDir}/).
|
||||
*/
|
||||
gulp.task('sorcery', ['browserify'], function () {
|
||||
let chain = sorcery.loadSync(`${buildDir}/xterm.js`);
|
||||
chain.apply();
|
||||
chain.writeSync();
|
||||
});
|
||||
|
||||
gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
||||
addons.forEach((addon) => {
|
||||
const chain = sorcery.loadSync(`${buildDir}/addons/${addon}/${addon}.js`);
|
||||
chain.apply();
|
||||
chain.writeSync();
|
||||
})
|
||||
});
|
||||
|
||||
gulp.task('build', ['css', 'sorcery', 'sorcery-addons']);
|
||||
gulp.task('test', ['mocha']);
|
||||
gulp.task('default', ['build']);
|
||||
+12
-28
@@ -2,7 +2,7 @@
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.14.0",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"main": "lib/xterm.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
@@ -15,59 +15,43 @@
|
||||
"@types/puppeteer": "^1.12.4",
|
||||
"@types/utf8": "^2.1.6",
|
||||
"@types/webpack": "^4.4.11",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.0",
|
||||
"coveralls": "^3.0.1",
|
||||
"express": "4.13.4",
|
||||
"express-ws": "2.0.0-rc.1",
|
||||
"fs-extra": "^1.0.0",
|
||||
"glob": "^7.0.5",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-cli": "^1.2.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-mocha": "^3.0.1",
|
||||
"gulp-sourcemaps": "1.9.1",
|
||||
"gulp-typescript": "^3.1.3",
|
||||
"gulp-util": "3.0.8",
|
||||
"jsdom": "^11.11.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"node-pty": "0.7.6",
|
||||
"nodemon": "1.10.2",
|
||||
"nyc": "^11.8.0",
|
||||
"puppeteer": "^1.15.0",
|
||||
"sorcery": "^0.10.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"ts-loader": "^4.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "3.5",
|
||||
"utf8": "^3.0.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"xterm-addon-attach": "0.1.0-beta8",
|
||||
"xterm-addon-search": "0.1.0-beta4",
|
||||
"xterm-addon-web-links": "0.1.0-beta6",
|
||||
"zmodem.js": "^0.1.5"
|
||||
"xterm-addon-web-links": "0.1.0-beta6"
|
||||
},
|
||||
"scripts": {
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
"start": "node demo/start",
|
||||
"start-debug": "node --inspect-brk demo/start",
|
||||
"start-zmodem": "node demo/zmodem/app",
|
||||
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||
"test": "npm run mocha",
|
||||
"test-unit": "node ./bin/test.js",
|
||||
"test": "npm run test-unit",
|
||||
"posttest": "npm run lint",
|
||||
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
||||
"test-suite": "gulp mocha-suite --test",
|
||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||
"test-api": "mocha \"**/*.api.js\"",
|
||||
"mocha": "gulp test",
|
||||
"prebuild": "tsc -b ./src/tsconfig.all.json",
|
||||
"build": "gulp build",
|
||||
"prepare": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"build": "tsc -b ./src/tsconfig.all.json",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm run package",
|
||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||
"watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput"
|
||||
},
|
||||
"dependencies": {
|
||||
"mocha": "^6.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
import * as Strings from './Strings';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { isMac } from './common/Platform';
|
||||
import { RenderDebouncer } from './ui/RenderDebouncer';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { RenderDebouncer } from 'ui/RenderDebouncer';
|
||||
import { addDisposableDomListener } from 'ui/Lifecycle';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'ui/ScreenDprMonitor';
|
||||
import { IRenderDimensions } from './renderer/Types';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@
|
||||
import { assert, expect } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { CircularList } from 'common/CircularList';
|
||||
import { MockTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { BufferLine, CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||
import { BufferLine, CellData, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
|
||||
+5
-5
@@ -3,12 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CircularList, IInsertEvent } from './common/CircularList';
|
||||
import { CircularList, IInsertEvent } from 'common/CircularList';
|
||||
import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
|
||||
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
|
||||
import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './core/buffer/BufferReflow';
|
||||
import { Marker } from './core/buffer/Marker';
|
||||
import { IBufferLine, ICellData, IAttributeData } from 'core/Types';
|
||||
import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine';
|
||||
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'core/buffer/BufferReflow';
|
||||
import { Marker } from 'core/buffer/Marker';
|
||||
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { ITerminal, IBufferSet, IBuffer } from './Types';
|
||||
import { IAttributeData } from './core/Types';
|
||||
import { IAttributeData } from 'core/Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
import { EventEmitter2, IEvent } from 'common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ICharMeasure, ITerminalOptions } from './Types';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
import { EventEmitter2, IEvent } from 'common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* Utility class that measures the size of a character. Measurements are done in
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TestTerminal } from './TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { getStringCellWidth, wcwidth } from './CharWidth';
|
||||
import { IBuffer } from './Types';
|
||||
import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './core/buffer/BufferLine';
|
||||
import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from 'core/buffer/BufferLine';
|
||||
|
||||
|
||||
describe('getStringCellWidth', function(): void {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { fill } from './common/TypedArrayUtils';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
|
||||
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
|
||||
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ParserState, IDcsHandler, IParsingState } from './Types';
|
||||
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from './EscapeSequenceParser';
|
||||
import * as chai from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder';
|
||||
import { StringToUtf32, stringFromCodePoint } from 'core/input/TextDecoder';
|
||||
|
||||
function r(a: number, b: number): string[] {
|
||||
let c = b - a;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user