Merge branch 'master' into feat-jar-plugin
This commit is contained in:
commit
47492949e9
618
package-lock.json
generated
618
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -19,23 +19,24 @@
|
|||||||
"@soerenmartius/vue3-clipboard": "^0.1",
|
"@soerenmartius/vue3-clipboard": "^0.1",
|
||||||
"leaflet": "git+https://github.com/JLyne/leaflet.git",
|
"leaflet": "git+https://github.com/JLyne/leaflet.git",
|
||||||
"modern-normalize": "^1.1.0",
|
"modern-normalize": "^1.1.0",
|
||||||
"vue": "^3.2.20",
|
"vue": "^3.2.21",
|
||||||
"vuex": "^4.0"
|
"vuex": "^4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/dynmap": "^3.1.1",
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/jest-in-case": "^1.0.5",
|
"@types/jest-in-case": "^1.0.5",
|
||||||
"@types/leaflet": "1.7.5",
|
"@types/leaflet": "1.7.5",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.3",
|
"@typescript-eslint/eslint-plugin": "^5.3",
|
||||||
"@typescript-eslint/parser": "^5.3",
|
"@typescript-eslint/parser": "^5.3",
|
||||||
"@vitejs/plugin-vue": "^1.9",
|
"@vitejs/plugin-vue": "^1.9",
|
||||||
"@vue/compiler-sfc": "^3.2.18",
|
"@vue/compiler-sfc": "^3.2.21",
|
||||||
"@vue/eslint-config-typescript": "^9.0",
|
"@vue/eslint-config-typescript": "^9.0",
|
||||||
"@vue/test-utils": "^2.0.0-rc.16",
|
"@vue/test-utils": "^2.0.0-rc.16",
|
||||||
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
"@vue/vue3-jest": "^27.0.0-alpha.1",
|
||||||
"cpy-cli": "^3.1.1",
|
"cpy-cli": "^3.1.1",
|
||||||
"eslint": "^8.1",
|
"eslint": "^8.2",
|
||||||
"eslint-plugin-vue": "^8.0",
|
"eslint-plugin-vue": "^8.0",
|
||||||
"jest": "^27.3.1",
|
"jest": "^27.3.1",
|
||||||
"jest-in-case": "^1.0.2",
|
"jest-in-case": "^1.0.2",
|
||||||
@ -49,7 +50,7 @@
|
|||||||
"typescript": "^4.4",
|
"typescript": "^4.4",
|
||||||
"vite": "^2.6.10",
|
"vite": "^2.6.10",
|
||||||
"vite-plugin-svg-sprite-component": "^1.0",
|
"vite-plugin-svg-sprite-component": "^1.0",
|
||||||
"vue-tsc": "0.28.7"
|
"vue-tsc": "0.29.4"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<li :class="`message message--${message.type}`">
|
<li :class="`message message--${message.type}`">
|
||||||
<img v-if="showFace" width="16" height="16" class="message__face" :src="image" alt="" />
|
<img v-if="showFace" width="16" height="16" class="message__face" :src="image" alt="" />
|
||||||
|
<span v-if="messageChannel" class="message__channel" v-html="messageChannel"></span>
|
||||||
<span v-if="showSender" class="message__sender" v-html="message.playerName"></span>
|
<span v-if="showSender" class="message__sender" v-html="message.playerName"></span>
|
||||||
<span class="message__content" v-html="messageContent"></span>
|
<span class="message__content" v-html="messageContent"></span>
|
||||||
</li>
|
</li>
|
||||||
@ -41,6 +42,7 @@
|
|||||||
let image = ref(defaultImage),
|
let image = ref(defaultImage),
|
||||||
showFace = computed(() => store.state.components.chatBox?.showPlayerFaces && props.message.playerAccount),
|
showFace = computed(() => store.state.components.chatBox?.showPlayerFaces && props.message.playerAccount),
|
||||||
showSender = computed(() => props.message.playerName && props.message.type === 'chat'),
|
showSender = computed(() => props.message.playerName && props.message.type === 'chat'),
|
||||||
|
messageChannel = computed(() => props.message.type === 'chat' ? props.message.channel : undefined),
|
||||||
messageContent = computed(() => {
|
messageContent = computed(() => {
|
||||||
switch(props.message.type) {
|
switch(props.message.type) {
|
||||||
case 'chat':
|
case 'chat':
|
||||||
@ -73,6 +75,7 @@
|
|||||||
image,
|
image,
|
||||||
showFace,
|
showFace,
|
||||||
showSender,
|
showSender,
|
||||||
|
messageChannel,
|
||||||
messageContent
|
messageContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,10 +90,23 @@
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message__channel,
|
||||||
.message__sender {
|
.message__sender {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message__channel {
|
||||||
|
&:not(:empty):before {
|
||||||
|
content: '[';
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:empty):after {
|
||||||
|
content: ']';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message__sender {
|
||||||
&:not(:empty):after {
|
&:not(:empty):after {
|
||||||
content: ': ';
|
content: ': ';
|
||||||
}
|
}
|
||||||
|
@ -25,18 +25,7 @@ import {computed, watch} from "@vue/runtime-core";
|
|||||||
import {ComputedRef} from "@vue/reactivity";
|
import {ComputedRef} from "@vue/reactivity";
|
||||||
import {WatchStopHandle} from "vue";
|
import {WatchStopHandle} from "vue";
|
||||||
import {ActionTypes} from "@/store/action-types";
|
import {ActionTypes} from "@/store/action-types";
|
||||||
|
import {TileInformation} from "dynmap";
|
||||||
export interface TileInfo {
|
|
||||||
prefix: string;
|
|
||||||
nightday: string;
|
|
||||||
scaledx: number;
|
|
||||||
scaledy: number;
|
|
||||||
zoom: string;
|
|
||||||
zoomprefix: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
fmt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
@ -160,7 +149,7 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
|||||||
return 'z'.repeat(amount) + (amount === 0 ? '' : '_');
|
return 'z'.repeat(amount) + (amount === 0 ? '' : '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTileInfo(coords: Coordinate): TileInfo {
|
private getTileInfo(coords: Coordinate): TileInformation {
|
||||||
// zoom: max zoomed in = this.options.maxZoom, max zoomed out = 0
|
// zoom: max zoomed in = this.options.maxZoom, max zoomed out = 0
|
||||||
// izoom: max zoomed in = 0, max zoomed out = this.options.maxZoom
|
// izoom: max zoomed in = 0, max zoomed out = this.options.maxZoom
|
||||||
// zoomoutlevel: izoom < mapzoomin -> 0, else -> izoom - mapzoomin (which ranges from 0 till mapzoomout)
|
// zoomoutlevel: izoom < mapzoomin -> 0, else -> izoom - mapzoomin (which ranges from 0 till mapzoomout)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import {Coordinate, LiveAtlasWorldDefinition} from "@/index";
|
import {Coordinate, LiveAtlasWorldDefinition} from "@/index";
|
||||||
import {LatLng} from "leaflet";
|
import {LatLng} from "leaflet";
|
||||||
import {LiveAtlasProjection} from "@/model/LiveAtlasProjection";
|
import {LiveAtlasProjection} from "@/model/LiveAtlasProjection";
|
||||||
|
import {ImageFormat} from "dynmap";
|
||||||
|
|
||||||
export interface LiveAtlasMapDefinitionOptions {
|
export interface LiveAtlasMapDefinitionOptions {
|
||||||
world: LiveAtlasWorldDefinition;
|
world: LiveAtlasWorldDefinition;
|
||||||
@ -27,7 +28,7 @@ export interface LiveAtlasMapDefinitionOptions {
|
|||||||
nightAndDay?: boolean;
|
nightAndDay?: boolean;
|
||||||
backgroundDay?: string;
|
backgroundDay?: string;
|
||||||
backgroundNight?: string;
|
backgroundNight?: string;
|
||||||
imageFormat: string;
|
imageFormat: ImageFormat;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
mapToWorld?: [number, number, number, number, number, number, number, number, number];
|
mapToWorld?: [number, number, number, number, number, number, number, number, number];
|
||||||
worldToMap?: [number, number, number, number, number, number, number, number, number];
|
worldToMap?: [number, number, number, number, number, number, number, number, number];
|
||||||
@ -45,7 +46,7 @@ export default class LiveAtlasMapDefinition {
|
|||||||
readonly nightAndDay: boolean;
|
readonly nightAndDay: boolean;
|
||||||
readonly backgroundDay?: string;
|
readonly backgroundDay?: string;
|
||||||
readonly backgroundNight?: string;
|
readonly backgroundNight?: string;
|
||||||
readonly imageFormat: string;
|
readonly imageFormat: ImageFormat;
|
||||||
readonly prefix: string;
|
readonly prefix: string;
|
||||||
private readonly projection?: Readonly<LiveAtlasProjection>;
|
private readonly projection?: Readonly<LiveAtlasProjection>;
|
||||||
readonly nativeZoomLevels: number;
|
readonly nativeZoomLevels: number;
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
buildServerConfig, buildUpdates, buildWorlds
|
buildServerConfig, buildUpdates, buildWorlds
|
||||||
} from "@/util/dynmap";
|
} from "@/util/dynmap";
|
||||||
import {getImagePixelSize} from "@/util";
|
import {getImagePixelSize} from "@/util";
|
||||||
|
import {MarkerSet} from "dynmap";
|
||||||
|
|
||||||
export default class DynmapMapProvider extends MapProvider {
|
export default class DynmapMapProvider extends MapProvider {
|
||||||
private configurationAbort?: AbortController = undefined;
|
private configurationAbort?: AbortController = undefined;
|
||||||
@ -69,7 +70,7 @@ export default class DynmapMapProvider extends MapProvider {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const set = response.sets[key],
|
const set: MarkerSet = response.sets[key],
|
||||||
markers = buildMarkers(set.markers || {}),
|
markers = buildMarkers(set.markers || {}),
|
||||||
circles = buildCircles(set.circles || {}),
|
circles = buildCircles(set.circles || {}),
|
||||||
areas = buildAreas(set.areas || {}),
|
areas = buildAreas(set.areas || {}),
|
||||||
|
@ -208,5 +208,5 @@ const decodeTextarea = document.createElement('textarea');
|
|||||||
|
|
||||||
export const decodeHTMLEntities = (text: string) => {
|
export const decodeHTMLEntities = (text: string) => {
|
||||||
decodeTextarea.innerHTML = text;
|
decodeTextarea.innerHTML = text;
|
||||||
return decodeTextarea.textContent;
|
return decodeTextarea.textContent || '';
|
||||||
}
|
}
|
||||||
|
@ -31,20 +31,36 @@ import {getPoints} from "@/util/areas";
|
|||||||
import {decodeHTMLEntities, endWorldNameRegex, netherWorldNameRegex, titleColoursRegex} from "@/util";
|
import {decodeHTMLEntities, endWorldNameRegex, netherWorldNameRegex, titleColoursRegex} from "@/util";
|
||||||
import {getLinePoints} from "@/util/lines";
|
import {getLinePoints} from "@/util/lines";
|
||||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||||
|
import {
|
||||||
|
Configuration,
|
||||||
|
Marker, MarkerArea, MarkerCircle, MarkerLine, MarkerSet,
|
||||||
|
Options,
|
||||||
|
WorldConfiguration,
|
||||||
|
WorldMapConfiguration
|
||||||
|
} from "dynmap";
|
||||||
|
import {PointTuple} from "leaflet";
|
||||||
|
|
||||||
|
export function buildServerConfig(response: Options): LiveAtlasServerConfig {
|
||||||
|
let title = 'Dynmap';
|
||||||
|
|
||||||
|
if(response.title) {
|
||||||
|
title = response.title.replace(titleColoursRegex, '') || title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const followZoom = parseInt(response.followzoom || "", 10);
|
||||||
|
|
||||||
export function buildServerConfig(response: any): LiveAtlasServerConfig {
|
|
||||||
return {
|
return {
|
||||||
defaultMap: response.defaultmap || undefined,
|
defaultMap: response.defaultmap || undefined,
|
||||||
defaultWorld: response.defaultworld || undefined,
|
defaultWorld: response.defaultworld || undefined,
|
||||||
defaultZoom: response.defaultzoom || 0,
|
defaultZoom: response.defaultzoom || 0,
|
||||||
followMap: response.followmap || undefined,
|
followMap: response.followmap || undefined,
|
||||||
followZoom: response.followzoom,
|
followZoom: isNaN(followZoom) ? undefined : followZoom,
|
||||||
title: response.title.replace(titleColoursRegex, '') || 'Dynmap',
|
title: title,
|
||||||
expandUI: response.sidebaropened && response.sidebaropened !== 'false', //Sent as a string for some reason
|
expandUI: !!response.sidebaropened && response.sidebaropened !== 'false', //Sent as a string for some reason
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMessagesConfig(response: any): LiveAtlasServerMessageConfig {
|
export function buildMessagesConfig(response: Options): LiveAtlasServerMessageConfig {
|
||||||
return {
|
return {
|
||||||
chatPlayerJoin: response.joinmessage || '',
|
chatPlayerJoin: response.joinmessage || '',
|
||||||
chatPlayerQuit: response.quitmessage || '',
|
chatPlayerQuit: response.quitmessage || '',
|
||||||
@ -58,11 +74,11 @@ export function buildMessagesConfig(response: any): LiveAtlasServerMessageConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWorlds(response: any): Array<LiveAtlasWorldDefinition> {
|
export function buildWorlds(response: Configuration): Array<LiveAtlasWorldDefinition> {
|
||||||
const worlds: Map<string, LiveAtlasWorldDefinition> = new Map<string, LiveAtlasWorldDefinition>();
|
const worlds: Map<string, LiveAtlasWorldDefinition> = new Map<string, LiveAtlasWorldDefinition>();
|
||||||
|
|
||||||
//Get all the worlds first so we can handle append_to_world properly
|
//Get all the worlds first so we can handle append_to_world properly
|
||||||
(response.worlds || []).forEach((world: any) => {
|
(response.worlds || []).forEach((world: WorldConfiguration) => {
|
||||||
let worldType: LiveAtlasDimension = 'overworld';
|
let worldType: LiveAtlasDimension = 'overworld';
|
||||||
|
|
||||||
if (netherWorldNameRegex.test(world.name) || (world.name == 'DIM-1')) {
|
if (netherWorldNameRegex.test(world.name) || (world.name == 'DIM-1')) {
|
||||||
@ -85,9 +101,10 @@ export function buildWorlds(response: any): Array<LiveAtlasWorldDefinition> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
(response.worlds || []).forEach((world: any) => {
|
(response.worlds || []).forEach((world: WorldConfiguration) => {
|
||||||
(world.maps || []).forEach((map: any) => {
|
(world.maps || []).forEach((map: WorldMapConfiguration) => {
|
||||||
const actualWorld = worlds.get(world.name),
|
const actualWorld = worlds.get(world.name),
|
||||||
|
// @ts-ignore
|
||||||
assignedWorldName = map.append_to_world || world.name, //handle append_to_world
|
assignedWorldName = map.append_to_world || world.name, //handle append_to_world
|
||||||
assignedWorld = worlds.get(assignedWorldName);
|
assignedWorld = worlds.get(assignedWorldName);
|
||||||
|
|
||||||
@ -101,7 +118,7 @@ export function buildWorlds(response: any): Array<LiveAtlasWorldDefinition> {
|
|||||||
background: map.background || '#000000',
|
background: map.background || '#000000',
|
||||||
backgroundDay: map.backgroundday || '#000000',
|
backgroundDay: map.backgroundday || '#000000',
|
||||||
backgroundNight: map.backgroundnight || '#000000',
|
backgroundNight: map.backgroundnight || '#000000',
|
||||||
icon: map.icon || undefined,
|
icon: (map.icon || undefined) as string | undefined,
|
||||||
imageFormat: map['image-format'] || 'png',
|
imageFormat: map['image-format'] || 'png',
|
||||||
name: map.name || '(Unnamed map)',
|
name: map.name || '(Unnamed map)',
|
||||||
nightAndDay: map.nightandday || false,
|
nightAndDay: map.nightandday || false,
|
||||||
@ -118,7 +135,7 @@ export function buildWorlds(response: any): Array<LiveAtlasWorldDefinition> {
|
|||||||
return Array.from(worlds.values());
|
return Array.from(worlds.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildComponents(response: any): LiveAtlasComponentConfig {
|
export function buildComponents(response: Configuration): LiveAtlasComponentConfig {
|
||||||
const components: LiveAtlasComponentConfig = {
|
const components: LiveAtlasComponentConfig = {
|
||||||
markers: {
|
markers: {
|
||||||
showLabels: false,
|
showLabels: false,
|
||||||
@ -130,7 +147,7 @@ export function buildComponents(response: any): LiveAtlasComponentConfig {
|
|||||||
showImages: response.showplayerfacesinmenu || false,
|
showImages: response.showplayerfacesinmenu || false,
|
||||||
},
|
},
|
||||||
coordinatesControl: undefined,
|
coordinatesControl: undefined,
|
||||||
layerControl: response.showlayercontrol && response.showlayercontrol !== 'false', //Sent as a string for some reason
|
layerControl: !!response.showlayercontrol && response.showlayercontrol !== 'false', //Sent as a string for some reason
|
||||||
linkControl: false,
|
linkControl: false,
|
||||||
clockControl: undefined,
|
clockControl: undefined,
|
||||||
logoControls: [],
|
logoControls: [],
|
||||||
@ -237,7 +254,7 @@ export function buildComponents(response: any): LiveAtlasComponentConfig {
|
|||||||
return components;
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMarkerSet(id: string, data: any): any {
|
export function buildMarkerSet(id: string, data: MarkerSet): any {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: data.label || "Unnamed set",
|
label: data.label || "Unnamed set",
|
||||||
@ -263,7 +280,17 @@ export function buildMarkers(data: any): Map<string, LiveAtlasMarker> {
|
|||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMarker(data: any): LiveAtlasMarker {
|
export function buildMarker(data: Marker): LiveAtlasMarker {
|
||||||
|
let dimensions;
|
||||||
|
|
||||||
|
if(data.dim) {
|
||||||
|
dimensions = data.dim.split('x').slice(0, 2).map(value => parseInt(value));
|
||||||
|
|
||||||
|
if(dimensions.length !== 2) {
|
||||||
|
dimensions = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const marker = Object.seal({
|
const marker = Object.seal({
|
||||||
label: data.label || '',
|
label: data.label || '',
|
||||||
isLabelHTML: data.markup || false,
|
isLabelHTML: data.markup || false,
|
||||||
@ -272,7 +299,7 @@ export function buildMarker(data: any): LiveAtlasMarker {
|
|||||||
y: data.y || 0,
|
y: data.y || 0,
|
||||||
z: data.z || 0,
|
z: data.z || 0,
|
||||||
},
|
},
|
||||||
dimensions: data.dim ? data.dim.split('x') : [16, 16],
|
dimensions: (dimensions || [16, 16]) as PointTuple,
|
||||||
icon: data.icon || "default",
|
icon: data.icon || "default",
|
||||||
minZoom: typeof data.minzoom !== 'undefined' && data.minzoom > -1 ? data.minzoom : undefined,
|
minZoom: typeof data.minzoom !== 'undefined' && data.minzoom > -1 ? data.minzoom : undefined,
|
||||||
maxZoom: typeof data.maxzoom !== 'undefined' && data.maxzoom > -1 ? data.maxzoom : undefined,
|
maxZoom: typeof data.maxzoom !== 'undefined' && data.maxzoom > -1 ? data.maxzoom : undefined,
|
||||||
@ -301,7 +328,7 @@ export function buildAreas(data: any): Map<string, LiveAtlasArea> {
|
|||||||
return areas;
|
return areas;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildArea(area: any): LiveAtlasArea {
|
export function buildArea(area: MarkerArea): LiveAtlasArea {
|
||||||
const opacity = area.fillopacity || 0,
|
const opacity = area.fillopacity || 0,
|
||||||
x = area.x || [0, 0],
|
x = area.x || [0, 0],
|
||||||
y: [number, number] = [area.ybottom || 0, area.ytop || 0],
|
y: [number, number] = [area.ybottom || 0, area.ytop || 0],
|
||||||
@ -339,7 +366,7 @@ export function buildLines(data: any): Map<string, LiveAtlasLine> {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildLine(line: any): LiveAtlasLine {
|
export function buildLine(line: MarkerLine): LiveAtlasLine {
|
||||||
return Object.seal({
|
return Object.seal({
|
||||||
style: {
|
style: {
|
||||||
color: line.color || '#ff0000',
|
color: line.color || '#ff0000',
|
||||||
@ -369,7 +396,7 @@ export function buildCircles(data: any): Map<string, LiveAtlasCircle> {
|
|||||||
return circles;
|
return circles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCircle(circle: any): LiveAtlasCircle {
|
export function buildCircle(circle: MarkerCircle): LiveAtlasCircle {
|
||||||
return Object.seal({
|
return Object.seal({
|
||||||
location: {
|
location: {
|
||||||
x: circle.x || 0,
|
x: circle.x || 0,
|
||||||
@ -487,7 +514,7 @@ export function buildUpdates(data: Array<any>, lastUpdate: Date) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.source !== 'player' && entry.source !== 'web') {
|
if (entry.source !== 'player' && entry.source !== 'web' && entry.source !== 'plugin') {
|
||||||
dropped.notImplemented++;
|
dropped.notImplemented++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user