init commit
This commit is contained in:
@@ -238,7 +238,7 @@ IScan.offScanListener("order");
|
|||||||
listener.cancel();
|
listener.cancel();
|
||||||
```
|
```
|
||||||
|
|
||||||
### `getStatus(): "scanning" | "closed"`
|
### `getStatus(): "scanning" | "ready"`
|
||||||
|
|
||||||
获取当前扫码状态。
|
获取当前扫码状态。
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ interface ScanResult {
|
|||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScanStatus = "scanning" | "closed";
|
type ScanStatus = "scanning" | "ready";
|
||||||
|
|
||||||
type ScanResultCallback = (result: ScanResult) => any;
|
type ScanResultCallback = (result: ScanResult) => any;
|
||||||
type ScanStatusCallback = (status: ScanStatus) => any;
|
type ScanStatusCallback = (status: ScanStatus) => any;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
|
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
|
||||||
<title>IScan Demo</title>
|
<title>IScan</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var vconsole = new VConsole();
|
var vconsole = new VConsole();
|
||||||
@@ -332,24 +332,20 @@ IScan.stopScan();</pre>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopScan() {
|
function stopScan() {
|
||||||
hide();
|
|
||||||
IScan.stopScan();
|
IScan.stopScan();
|
||||||
setStatus(IScan.getStatus());
|
setStatus(IScan.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
function startScan() {
|
function startScan() {
|
||||||
hide();
|
|
||||||
IScan.startScan();
|
IScan.startScan();
|
||||||
setStatus(IScan.getStatus());
|
setStatus(IScan.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanImage() {
|
function scanImage() {
|
||||||
hide();
|
|
||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanVideo() {
|
function scanVideo() {
|
||||||
hide();
|
|
||||||
IScan.scanVideo();
|
IScan.scanVideo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
dist/index.d.ts
vendored
6
dist/index.d.ts
vendored
@@ -2,6 +2,10 @@
|
|||||||
* 扫码初始化选项
|
* 扫码初始化选项
|
||||||
*/
|
*/
|
||||||
interface ScanConfigOptions {
|
interface ScanConfigOptions {
|
||||||
|
/**
|
||||||
|
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||||
|
*/
|
||||||
|
scanRestartDelay?: number,
|
||||||
/**
|
/**
|
||||||
* 桥接是否启用,默认启用
|
* 桥接是否启用,默认启用
|
||||||
*/
|
*/
|
||||||
@@ -138,7 +142,7 @@ interface ScanListenerInfo {
|
|||||||
/**
|
/**
|
||||||
* 监听状态
|
* 监听状态
|
||||||
*/
|
*/
|
||||||
type ScanStatus = "scanning" | "closed";
|
type ScanStatus = "scanning" | "ready";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听结果回调
|
* 监听结果回调
|
||||||
|
|||||||
6
dist/index.html
vendored
6
dist/index.html
vendored
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,minimum-scale=1,maximum-scale=1,user-scalable=no"/><title>IScan Demo</title><script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script><script>var vconsole = new VConsole();</script><style>* {
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,minimum-scale=1,maximum-scale=1,user-scalable=no"/><title>IScan</title><script src="https://cdn.jsdelivr.net/npm/vconsole@3.12.0/dist/vconsole.min.js"></script><script>var vconsole = new VConsole();</script><style>* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,24 +262,20 @@ IScan.stopScan();</pre></section></main><script>(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopScan() {
|
function stopScan() {
|
||||||
hide();
|
|
||||||
IScan.stopScan();
|
IScan.stopScan();
|
||||||
setStatus(IScan.getStatus());
|
setStatus(IScan.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
function startScan() {
|
function startScan() {
|
||||||
hide();
|
|
||||||
IScan.startScan();
|
IScan.startScan();
|
||||||
setStatus(IScan.getStatus());
|
setStatus(IScan.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanImage() {
|
function scanImage() {
|
||||||
hide();
|
|
||||||
IScan.scanImage();
|
IScan.scanImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanVideo() {
|
function scanVideo() {
|
||||||
hide();
|
|
||||||
IScan.scanVideo();
|
IScan.scanVideo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,17 +1,22 @@
|
|||||||
import { inRuntime, bridgeAsync } from "../bridge";
|
import { inRuntime, bridgeAsync } from "../bridge";
|
||||||
import { isSupportWebScan, startScanForWeb, stopScanForWeb, isSupportImageScan, startScanForImage } from "../web";
|
import { isSupportWebScan, startScanForWeb, stopScanForWeb, isSupportImageScan, startScanForImage, unlockScanBeep } from "../web";
|
||||||
import { isSupportWxScan, startScanForWx } from "../wx";
|
import { isSupportWxScan, startScanForWx } from "../wx";
|
||||||
import { startScanner, stopScanner } from "../scanner";
|
import { startScanner, stopScanner } from "../scanner";
|
||||||
import { getConfig } from "../config";
|
import { getConfig } from "../config";
|
||||||
|
import { toAny } from "../../utils/toany";
|
||||||
|
|
||||||
let _scan_status = "closed";
|
let _scan_status = "ready";
|
||||||
let _scan_status_listener = null;
|
let _scan_status_listener = null;
|
||||||
let _scan_listener_list = [];
|
let _scan_listener_list = [];
|
||||||
let _scan_resolve = null;
|
let _scan_resolve = null;
|
||||||
let _scan_closing = false;
|
let _scan_closing = false;
|
||||||
let _scan_next_start_time = 0;
|
let _scan_next_start_time = 0;
|
||||||
|
|
||||||
const SCAN_RESTART_DELAY = 2000;
|
const SCAN_RESTART_DELAY = 500;
|
||||||
|
|
||||||
|
function getScanRestartDelay() {
|
||||||
|
return toAny(getConfig("scanRestartDelay"), SCAN_RESTART_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
function __checkScanner() {
|
function __checkScanner() {
|
||||||
if (_scan_listener_list.length > 0) {
|
if (_scan_listener_list.length > 0) {
|
||||||
@@ -46,7 +51,7 @@ function __result(result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matched) {
|
if (matched) {
|
||||||
_scan_next_start_time = Date.now() + SCAN_RESTART_DELAY;
|
_scan_next_start_time = Date.now() + getScanRestartDelay();
|
||||||
}
|
}
|
||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
@@ -71,10 +76,10 @@ function __scanning() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __closed() {
|
function __closed() {
|
||||||
if (_scan_status !== "closed") {
|
if (_scan_status !== "ready") {
|
||||||
_scan_status = "closed";
|
_scan_status = "ready";
|
||||||
if (_scan_status_listener) {
|
if (_scan_status_listener) {
|
||||||
_scan_status_listener({ status: "closed" });
|
_scan_status_listener({ status: "ready" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,6 +337,7 @@ export function startScan() {
|
|||||||
scanPromise = __startWxScan();
|
scanPromise = __startWxScan();
|
||||||
} else if (isSupportWebScan()) {
|
} else if (isSupportWebScan()) {
|
||||||
console.log("startScanForWeb");
|
console.log("startScanForWeb");
|
||||||
|
unlockScanBeep();
|
||||||
scanPromise = startScanForWeb(getConfig("webScanCanvasStyle"), __result);
|
scanPromise = startScanForWeb(getConfig("webScanCanvasStyle"), __result);
|
||||||
} else if (isSupportImageScan()) {
|
} else if (isSupportImageScan()) {
|
||||||
console.log("startScanForImage");
|
console.log("startScanForImage");
|
||||||
@@ -351,6 +357,10 @@ export function scanVideo() {
|
|||||||
console.log("Not support video scanner");
|
console.log("Not support video scanner");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isScanning() || _scan_closing || Date.now() < _scan_next_start_time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unlockScanBeep();
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
__scanning();
|
__scanning();
|
||||||
return startScanForWeb(getConfig("webScanCanvasStyle"), __result).then(resp => {
|
return startScanForWeb(getConfig("webScanCanvasStyle"), __result).then(resp => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
let _scannerCallback = null;
|
let _scannerCallback = null;
|
||||||
let _scannerStatus = "closed";
|
let _scannerStatus = "ready";
|
||||||
let _scannerValue = "";
|
let _scannerValue = "";
|
||||||
let _scannerTimer = null;
|
let _scannerTimer = null;
|
||||||
let _scannerLastInputTime = 0;
|
let _scannerLastInputTime = 0;
|
||||||
@@ -82,7 +82,7 @@ export function stopScanner(){
|
|||||||
if (_scannerStatus !== "scanning") {
|
if (_scannerStatus !== "scanning") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_scannerStatus = "closed";
|
_scannerStatus = "ready";
|
||||||
_scannerCallback = null;
|
_scannerCallback = null;
|
||||||
clearScannerValue();
|
clearScannerValue();
|
||||||
window.removeEventListener("keydown", onScannerKeydown);
|
window.removeEventListener("keydown", onScannerKeydown);
|
||||||
|
|||||||
@@ -5,17 +5,25 @@ import scanBeepAudio from "../../../res/scan_beep.ogg";
|
|||||||
|
|
||||||
const scanWeb = {
|
const scanWeb = {
|
||||||
uuid: null,
|
uuid: null,
|
||||||
finish: true
|
finish: true,
|
||||||
|
stream: null,
|
||||||
|
videoEl: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SCAN_BEEP_AUDIO = scanBeepAudio;
|
const DEFAULT_SCAN_BEEP_AUDIO = scanBeepAudio;
|
||||||
const ZXING_READER_WASM_URL = "./lib/reader.wasm";
|
const ZXING_READER_WASM_URL = "./lib/reader.wasm";
|
||||||
|
|
||||||
let barcodeDetectorPreparePromise = null;
|
let barcodeDetectorPreparePromise = null;
|
||||||
|
let scanBeepAudioEl = null;
|
||||||
|
let scanBeepAudioSrc = null;
|
||||||
|
let scanBeepUnlocked = false;
|
||||||
|
|
||||||
function removeEl(id) {
|
function removeEl(id, uuid) {
|
||||||
try {
|
try {
|
||||||
let el = document.getElementById(id);
|
let el = document.getElementById(id);
|
||||||
|
if (uuid && el && el.uuid !== uuid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.body.removeChild(el);
|
document.body.removeChild(el);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
}
|
}
|
||||||
@@ -32,6 +40,32 @@ function createEl(tagName, id, style, appendChild) {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopMediaStream(stream) {
|
||||||
|
try {
|
||||||
|
const tracks = stream && stream.getTracks && stream.getTracks();
|
||||||
|
if (tracks && tracks.length) {
|
||||||
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
|
tracks[i].stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopActiveWebScan() {
|
||||||
|
scanWeb.uuid = null;
|
||||||
|
stopMediaStream(scanWeb.stream);
|
||||||
|
try {
|
||||||
|
if (scanWeb.videoEl) {
|
||||||
|
scanWeb.videoEl.pause && scanWeb.videoEl.pause();
|
||||||
|
scanWeb.videoEl.srcObject = null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
scanWeb.stream = null;
|
||||||
|
scanWeb.videoEl = null;
|
||||||
|
}
|
||||||
|
|
||||||
function transformPoint(point, width, height, mirrorHorizontal, mirrorVertical, cover) {
|
function transformPoint(point, width, height, mirrorHorizontal, mirrorVertical, cover) {
|
||||||
let x = point.x;
|
let x = point.x;
|
||||||
let y = point.y;
|
let y = point.y;
|
||||||
@@ -206,12 +240,14 @@ function playScanBeep() {
|
|||||||
if (getConfig("webScanBeepEnabled") === false) {
|
if (getConfig("webScanBeepEnabled") === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const audioSrc = getConfig("webScanBeepAudio") || DEFAULT_SCAN_BEEP_AUDIO;
|
const audio = getScanBeepAudio();
|
||||||
if (!audioSrc || typeof Audio === 'undefined') {
|
if (!audio) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const audio = new Audio(audioSrc);
|
audio.muted = false;
|
||||||
|
audio.volume = 1;
|
||||||
|
audio.currentTime = 0;
|
||||||
const playPromise = audio.play();
|
const playPromise = audio.play();
|
||||||
playPromise && playPromise.catch && playPromise.catch(() => {
|
playPromise && playPromise.catch && playPromise.catch(() => {
|
||||||
// no thing
|
// no thing
|
||||||
@@ -220,12 +256,45 @@ function playScanBeep() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScanBeepAudio() {
|
||||||
|
const audioSrc = getConfig("webScanBeepAudio") || DEFAULT_SCAN_BEEP_AUDIO;
|
||||||
|
if (!audioSrc || typeof Audio === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!scanBeepAudioEl || scanBeepAudioSrc !== audioSrc) {
|
||||||
|
scanBeepAudioSrc = audioSrc;
|
||||||
|
scanBeepUnlocked = false;
|
||||||
|
scanBeepAudioEl = new Audio(audioSrc);
|
||||||
|
scanBeepAudioEl.preload = "auto";
|
||||||
|
try {
|
||||||
|
scanBeepAudioEl.load && scanBeepAudioEl.load();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scanBeepAudioEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unlockScanBeep() {
|
||||||
|
if (getConfig("webScanBeepEnabled") === false || scanBeepUnlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const audio = getScanBeepAudio();
|
||||||
|
if (!audio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 这里只做预加载,不主动 play,避免部分浏览器露出提示音。
|
||||||
|
audio.load && audio.load();
|
||||||
|
scanBeepUnlocked = true;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isSupportWebScan() {
|
export function isSupportWebScan() {
|
||||||
return typeof navigator !== 'undefined'
|
return typeof navigator !== 'undefined'
|
||||||
&& navigator.mediaDevices
|
&& navigator.mediaDevices
|
||||||
&& navigator.mediaDevices.getUserMedia
|
&& navigator.mediaDevices.getUserMedia
|
||||||
&& !!getBarcodeDetectorClass()
|
&& !!getBarcodeDetectorClass();
|
||||||
&& getConfig("webCanvasEnabled") !== false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportImageScan() {
|
export function isSupportImageScan() {
|
||||||
@@ -236,7 +305,7 @@ export function isSupportImageScan() {
|
|||||||
|
|
||||||
export function stopScanForWeb() {
|
export function stopScanForWeb() {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
scanWeb.uuid = null;
|
stopActiveWebScan();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,8 +388,10 @@ export function startScanForImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function startScanForWeb(canvasStyle, onResult) {
|
export function startScanForWeb(canvasStyle, onResult) {
|
||||||
|
let currentUuid = null;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
stopActiveWebScan();
|
||||||
scanWeb.uuid = createUUID();
|
scanWeb.uuid = createUUID();
|
||||||
scanWeb.finish = false;
|
scanWeb.finish = false;
|
||||||
let videoEl = createEl("video",
|
let videoEl = createEl("video",
|
||||||
@@ -338,10 +409,11 @@ export function startScanForWeb(canvasStyle, onResult) {
|
|||||||
let canvasDisplaySize = getCanvasDisplaySize(canvasEl, 300, 240);
|
let canvasDisplaySize = getCanvasDisplaySize(canvasEl, 300, 240);
|
||||||
canvasEl.style.display = "none";
|
canvasEl.style.display = "none";
|
||||||
let context = canvasEl.getContext("2d");
|
let context = canvasEl.getContext("2d");
|
||||||
let currentUuid = scanWeb.uuid;
|
currentUuid = scanWeb.uuid;
|
||||||
videoEl.width = 300;
|
videoEl.width = 300;
|
||||||
videoEl.height = 300;
|
videoEl.height = 300;
|
||||||
videoEl.uuid = scanWeb.uuid;
|
videoEl.uuid = scanWeb.uuid;
|
||||||
|
canvasEl.uuid = scanWeb.uuid;
|
||||||
createBarcodeDetector(getConfig("webScanType")).then(detector => {
|
createBarcodeDetector(getConfig("webScanType")).then(detector => {
|
||||||
return navigator.mediaDevices.getUserMedia({
|
return navigator.mediaDevices.getUserMedia({
|
||||||
video: {
|
video: {
|
||||||
@@ -356,6 +428,13 @@ export function startScanForWeb(canvasStyle, onResult) {
|
|||||||
}).then(function (options) {
|
}).then(function (options) {
|
||||||
const detector = options.detector;
|
const detector = options.detector;
|
||||||
const stream = options.stream;
|
const stream = options.stream;
|
||||||
|
if (scanWeb.uuid !== currentUuid) {
|
||||||
|
stopMediaStream(stream);
|
||||||
|
reject({ cancel: 1 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scanWeb.stream = stream;
|
||||||
|
scanWeb.videoEl = videoEl;
|
||||||
const mirrorVideo = shouldMirrorWebVideo(stream);
|
const mirrorVideo = shouldMirrorWebVideo(stream);
|
||||||
const mirrorVideoVertical = shouldMirrorWebVideoVertical();
|
const mirrorVideoVertical = shouldMirrorWebVideoVertical();
|
||||||
videoEl.srcObject = stream;
|
videoEl.srcObject = stream;
|
||||||
@@ -370,8 +449,14 @@ export function startScanForWeb(canvasStyle, onResult) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
closed = true;
|
closed = true;
|
||||||
|
stopMediaStream(stream);
|
||||||
|
if (scanWeb.uuid === currentUuid || scanWeb.stream === stream) {
|
||||||
|
scanWeb.stream = null;
|
||||||
|
scanWeb.videoEl = null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
stream.getTracks()[0].stop();
|
videoEl.pause && videoEl.pause();
|
||||||
|
videoEl.srcObject = null;
|
||||||
} catch (_e) { }
|
} catch (_e) { }
|
||||||
};
|
};
|
||||||
let tick = () => {
|
let tick = () => {
|
||||||
@@ -443,7 +528,7 @@ export function startScanForWeb(canvasStyle, onResult) {
|
|||||||
reject({ error: e });
|
reject({ error: e });
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
removeEl("__webscan_video__");
|
removeEl("__webscan_video__", currentUuid);
|
||||||
removeEl("__webscan_canvas__");
|
removeEl("__webscan_canvas__", currentUuid);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
6
types/index.d.ts
vendored
6
types/index.d.ts
vendored
@@ -2,6 +2,10 @@
|
|||||||
* 扫码初始化选项
|
* 扫码初始化选项
|
||||||
*/
|
*/
|
||||||
interface ScanConfigOptions {
|
interface ScanConfigOptions {
|
||||||
|
/**
|
||||||
|
* 扫码重启延迟,单位:毫秒,默认500ms
|
||||||
|
*/
|
||||||
|
scanRestartDelay?: number,
|
||||||
/**
|
/**
|
||||||
* 桥接是否启用,默认启用
|
* 桥接是否启用,默认启用
|
||||||
*/
|
*/
|
||||||
@@ -138,7 +142,7 @@ interface ScanListenerInfo {
|
|||||||
/**
|
/**
|
||||||
* 监听状态
|
* 监听状态
|
||||||
*/
|
*/
|
||||||
type ScanStatus = "scanning" | "closed";
|
type ScanStatus = "scanning" | "ready";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听结果回调
|
* 监听结果回调
|
||||||
|
|||||||
Reference in New Issue
Block a user