Merge branch 'develop' of github.com:thecodingmachine/workadventure
This commit is contained in:
commit
52489f9c72
133
docs/maps/menu.php
Normal file
133
docs/maps/menu.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
$extraMenu = require __DIR__.'/../../scripting_api_extra_doc/menu.php';
|
||||||
|
$extraUtilsMenu = require __DIR__.'/../../scripting_api_extra_doc/menu_functions.php';
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => 'Getting started',
|
||||||
|
'url' => '/map-building',
|
||||||
|
'markdown' => 'maps.index'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'WorkAdventure maps',
|
||||||
|
'url' => '/map-building/wa-maps',
|
||||||
|
'markdown' => 'maps.wa-maps'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Entries and exits',
|
||||||
|
'url' => '/map-building/entry-exit.md',
|
||||||
|
'markdown' => 'maps.entry-exit'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Opening a website',
|
||||||
|
'url' => '/map-building/opening-a-website.md',
|
||||||
|
'markdown' => 'maps.opening-a-website'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Meeting rooms',
|
||||||
|
'url' => '/map-building/meeting-rooms.md',
|
||||||
|
'markdown' => 'maps.meeting-rooms'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Special zones',
|
||||||
|
'url' => '/map-building/special-zones.md',
|
||||||
|
'markdown' => 'maps.special-zones'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Animations',
|
||||||
|
'url' => '/map-building/animations.md',
|
||||||
|
'markdown' => 'maps.animations'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Integrated websites',
|
||||||
|
'url' => '/map-building/website-in-map.md',
|
||||||
|
'markdown' => 'maps.website-in-map'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Variables',
|
||||||
|
'url' => '/map-building/variables.md',
|
||||||
|
'markdown' => 'maps.variables'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Self-hosting your map',
|
||||||
|
'url' => '/map-building/hosting.md',
|
||||||
|
'markdown' => 'maps.hosting'
|
||||||
|
],
|
||||||
|
$extraMenu,
|
||||||
|
[
|
||||||
|
'title' => 'Scripting maps',
|
||||||
|
'url' => '/map-building/scripting',
|
||||||
|
'markdown' => 'maps.scripting',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'title' => 'Using Typescript',
|
||||||
|
'url' => '/map-building/using-typescript.md',
|
||||||
|
'markdown' => 'maps.using-typescript'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'API Reference',
|
||||||
|
'url' => '/map-building/api-reference',
|
||||||
|
'markdown' => 'maps.api-reference',
|
||||||
|
'collapse' => true,
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'title' => 'Initialization',
|
||||||
|
'url' => '/map-building/api-start.md',
|
||||||
|
'markdown' => 'maps.api-start',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Navigation',
|
||||||
|
'url' => '/map-building/api-nav.md',
|
||||||
|
'markdown' => 'maps.api-nav',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Chat',
|
||||||
|
'url' => '/map-building/api-chat.md',
|
||||||
|
'markdown' => 'maps.api-chat',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Room',
|
||||||
|
'url' => '/map-building/api-room.md',
|
||||||
|
'markdown' => 'maps.api-room',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'State',
|
||||||
|
'url' => '/map-building/api-state.md',
|
||||||
|
'markdown' => 'maps.api-state',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Player',
|
||||||
|
'url' => '/map-building/api-player.md',
|
||||||
|
'markdown' => 'maps.api-player',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'UI',
|
||||||
|
'url' => '/map-building/api-ui.md',
|
||||||
|
'markdown' => 'maps.api-ui',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Sound',
|
||||||
|
'url' => '/map-building/api-sound.md',
|
||||||
|
'markdown' => 'maps.api-sound',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Controls',
|
||||||
|
'url' => '/map-building/api-controls.md',
|
||||||
|
'markdown' => 'maps.api-controls',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Deprecated',
|
||||||
|
'url' => '/map-building/api-deprecated.md',
|
||||||
|
'markdown' => 'maps.api-deprecated',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
$extraUtilsMenu
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Troubleshooting',
|
||||||
|
'url' => '/map-building/troubleshooting',
|
||||||
|
'view' => 'content.map.troubleshooting'
|
||||||
|
],
|
||||||
|
];
|
@ -13,6 +13,7 @@ In order to create a zone that opens websites:
|
|||||||
* You must create a specific layer.
|
* You must create a specific layer.
|
||||||
* In layer properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://")
|
* In layer properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://")
|
||||||
* You may also use "`openWebsiteWidth`" property (of type "`number`" between 0 and 100) to control the width of the iframe.
|
* You may also use "`openWebsiteWidth`" property (of type "`number`" between 0 and 100) to control the width of the iframe.
|
||||||
|
* You may also use "`openTab`" property (of type "`string`") to open in a new tab instead.
|
||||||
|
|
||||||
{.alert.alert-warning}
|
{.alert.alert-warning}
|
||||||
A website can explicitly forbid another website from loading it in an iFrame using
|
A website can explicitly forbid another website from loading it in an iFrame using
|
||||||
|
2
front/dist/.htaccess
vendored
2
front/dist/.htaccess
vendored
@ -22,3 +22,5 @@ RewriteBase /
|
|||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteRule "^[_@]/" "/index.html" [L]
|
RewriteRule "^[_@]/" "/index.html" [L]
|
||||||
RewriteRule "^register/" "/index.html" [L]
|
RewriteRule "^register/" "/index.html" [L]
|
||||||
|
RewriteRule "^login" "/index.html" [L]
|
||||||
|
RewriteRule "^jwt/" "/index.html" [L]
|
||||||
|
@ -7,7 +7,13 @@
|
|||||||
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
|
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
|
||||||
import ContactSubMenu from "./ContactSubMenu.svelte";
|
import ContactSubMenu from "./ContactSubMenu.svelte";
|
||||||
import CustomSubMenu from "./CustomSubMenu.svelte"
|
import CustomSubMenu from "./CustomSubMenu.svelte"
|
||||||
import {customMenuIframe, menuVisiblilityStore, SubMenusInterface, subMenusStore} from "../../Stores/MenuStore";
|
import {
|
||||||
|
checkSubMenuToShow,
|
||||||
|
customMenuIframe,
|
||||||
|
menuVisiblilityStore,
|
||||||
|
SubMenusInterface,
|
||||||
|
subMenusStore
|
||||||
|
} from "../../Stores/MenuStore";
|
||||||
import {onDestroy, onMount} from "svelte";
|
import {onDestroy, onMount} from "svelte";
|
||||||
import {get} from "svelte/store";
|
import {get} from "svelte/store";
|
||||||
import type {Unsubscriber} from "svelte/store";
|
import type {Unsubscriber} from "svelte/store";
|
||||||
@ -25,6 +31,8 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
checkSubMenuToShow();
|
||||||
|
|
||||||
switchMenu(SubMenusInterface.settings);
|
switchMenu(SubMenusInterface.settings);
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -95,6 +103,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
|
<div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
|
||||||
|
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>×</button>
|
||||||
<h2>{activeSubMenu}</h2>
|
<h2>{activeSubMenu}</h2>
|
||||||
<svelte:component this={activeComponent} {...props}/>
|
<svelte:component this={activeComponent} {...props}/>
|
||||||
</div>
|
</div>
|
||||||
@ -110,9 +119,9 @@
|
|||||||
|
|
||||||
font-family: "Press Start 2P";
|
font-family: "Press Start 2P";
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
height: 80vh;
|
height: 80%;
|
||||||
width: 75vw;
|
width: 75%;
|
||||||
top: 10vh;
|
top: 10%;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@ -139,16 +148,34 @@
|
|||||||
div.menu-submenu-container {
|
div.menu-submenu-container {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
|
|
||||||
|
.nes-btn.is-error.close {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 800px) {
|
@media only screen and (max-width: 800px) {
|
||||||
div.menu-container-main {
|
div.menu-container-main {
|
||||||
--size-first-columns-grid: 120px;
|
--size-first-columns-grid: 120px;
|
||||||
height: 70vh;
|
height: 70%;
|
||||||
top: 55px;
|
top: 55px;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
font-size: 0.5em;
|
font-size: 0.5em;
|
||||||
|
|
||||||
|
div.menu-nav-sidebar {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.menu-submenu-container {
|
||||||
|
.nes-btn.is-error.close {
|
||||||
|
position: absolute;
|
||||||
|
top: -35px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -45,11 +45,6 @@
|
|||||||
enableCameraSceneVisibilityStore.showEnableCameraScene();
|
enableCameraSceneVisibilityStore.showEnableCameraScene();
|
||||||
gameManager.leaveGame(EnableCameraSceneName, new EnableCameraScene());
|
gameManager.leaveGame(EnableCameraSceneName, new EnableCameraScene());
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Uncomment when login will be completely developed
|
|
||||||
/*function clickLogin() {
|
|
||||||
connectionManager.loadOpenIDScreen();
|
|
||||||
}*/
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="customize-main">
|
<div class="customize-main">
|
||||||
@ -64,27 +59,25 @@
|
|||||||
</section>
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<section>
|
<section>
|
||||||
<a type="button" class="nes-btn" href="/login">Sing in</a>
|
<a type="button" class="nes-btn" href="/login">Sign in</a>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
<section>
|
<section>
|
||||||
<button type="button" class="nes-btn is-rounded" on:click|preventDefault={openEditSkinScene}>Edit Skin</button>
|
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>Edit Skin</button>
|
||||||
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
|
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>Setup camera</button>
|
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>Setup camera</button>
|
||||||
</section>
|
</section>
|
||||||
<!-- <section>
|
|
||||||
<button type="button" class="nes-btn is-primary" on:click|preventDefault={clickLogin}>Login</button>
|
|
||||||
</section>-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
div.customize-main {
|
div.customize-main{
|
||||||
section {
|
section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
@ -94,6 +94,7 @@ function changeNotification() {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
div.settings-main {
|
div.settings-main {
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 40px);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
section {
|
section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -112,14 +113,14 @@ function changeNotification() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
section.settings-section-noSaveOption {
|
section.settings-section-noSaveOption {
|
||||||
--nb-noSaveOptions: 2; //number of sub-element in the section
|
display: flex;
|
||||||
display: grid;
|
align-items: center;
|
||||||
grid-template-columns: calc(100% / var(--nb-noSaveOptions)) calc(100% / var(--nb-noSaveOptions)); //Same size for every sub-element
|
flex-wrap: wrap;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
flex: 1 1 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
margin: 0 0 15px;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,12 +130,6 @@ function changeNotification() {
|
|||||||
section {
|
section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.settings-section-noSaveOption {
|
|
||||||
height: 80px;
|
|
||||||
grid-template-columns: none;
|
|
||||||
grid-template-rows: calc(100% / var(--nb-noSaveOptions)) calc(100% / var(--nb-noSaveOptions)); //Same size for every sub-element;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -57,7 +57,7 @@ class ConnectionManager {
|
|||||||
loginSceneVisibleIframeStore.set(false);
|
loginSceneVisibleIframeStore.set(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}`;
|
const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}&playUri=${this._currentRoom.key}`;
|
||||||
window.location.assign(redirectUrl);
|
window.location.assign(redirectUrl);
|
||||||
return redirectUrl;
|
return redirectUrl;
|
||||||
}
|
}
|
||||||
@ -88,10 +88,9 @@ class ConnectionManager {
|
|||||||
this.connexionType = connexionType;
|
this.connexionType = connexionType;
|
||||||
this._currentRoom = null;
|
this._currentRoom = null;
|
||||||
if (connexionType === GameConnexionTypes.login) {
|
if (connexionType === GameConnexionTypes.login) {
|
||||||
//TODO clear all cash and redirect on login scene (iframe)
|
|
||||||
localUserStore.setAuthToken(null);
|
|
||||||
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
|
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
|
||||||
urlManager.pushRoomIdToUrl(this._currentRoom);
|
this.loadOpenIDScreen();
|
||||||
|
return Promise.reject(new Error("You will be redirect on login page"));
|
||||||
} else if (connexionType === GameConnexionTypes.jwt) {
|
} else if (connexionType === GameConnexionTypes.jwt) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const code = urlParams.get("code");
|
const code = urlParams.get("code");
|
||||||
@ -103,13 +102,14 @@ class ConnectionManager {
|
|||||||
throw "No Auth code provided";
|
throw "No Auth code provided";
|
||||||
}
|
}
|
||||||
localUserStore.setCode(code);
|
localUserStore.setCode(code);
|
||||||
|
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
|
||||||
try {
|
try {
|
||||||
await this.checkAuthUserConnexion();
|
await this.checkAuthUserConnexion();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.loadOpenIDScreen();
|
this.loadOpenIDScreen();
|
||||||
|
return Promise.reject(new Error("You will be redirect on login page"));
|
||||||
}
|
}
|
||||||
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
|
|
||||||
urlManager.pushRoomIdToUrl(this._currentRoom);
|
urlManager.pushRoomIdToUrl(this._currentRoom);
|
||||||
} else if (connexionType === GameConnexionTypes.register) {
|
} else if (connexionType === GameConnexionTypes.register) {
|
||||||
//@deprecated
|
//@deprecated
|
||||||
|
@ -71,7 +71,7 @@ function createSubMenusStore() {
|
|||||||
|
|
||||||
export const subMenusStore = createSubMenusStore();
|
export const subMenusStore = createSubMenusStore();
|
||||||
|
|
||||||
function checkSubMenuToShow() {
|
export function checkSubMenuToShow() {
|
||||||
if (!get(userIsAdminStore)) {
|
if (!get(userIsAdminStore)) {
|
||||||
subMenusStore.removeMenu(SubMenusInterface.globalMessages);
|
subMenusStore.removeMenu(SubMenusInterface.globalMessages);
|
||||||
}
|
}
|
||||||
@ -83,8 +83,6 @@ function checkSubMenuToShow() {
|
|||||||
subMenusStore.removeMenu(SubMenusInterface.aboutRoom);
|
subMenusStore.removeMenu(SubMenusInterface.aboutRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSubMenuToShow();
|
|
||||||
|
|
||||||
export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>();
|
export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>();
|
||||||
|
|
||||||
export function handleMenuRegistrationEvent(
|
export function handleMenuRegistrationEvent(
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
//TextGlobalMessage
|
//TextGlobalMessage
|
||||||
section.section-input-send-text {
|
section.section-input-send-text {
|
||||||
--height-toolbar: 15%;
|
--height-toolbar: 20%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.ql-toolbar{
|
.ql-toolbar{
|
||||||
height: var(--height-toolbar);
|
max-height: var(--height-toolbar);
|
||||||
background: whitesmoke;
|
background: whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1101,7 +1101,7 @@ div.is-silent {
|
|||||||
border-radius: 15px 15px 15px 15px;
|
border-radius: 15px 15px 15px 15px;
|
||||||
max-height: 20%;
|
max-height: 20%;
|
||||||
transition: right 350ms;
|
transition: right 350ms;
|
||||||
right: -20vw;
|
right: -300px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -27,13 +27,17 @@ export class AuthenticateController extends BaseController {
|
|||||||
console.warn("/message request was aborted");
|
console.warn("/message request was aborted");
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nonce, state } = parse(req.getQuery());
|
const { nonce, state, playUri } = parse(req.getQuery());
|
||||||
if (!state || !nonce) {
|
if (!state || !nonce) {
|
||||||
res.writeStatus("400 Unauthorized").end("missing state and nonce URL parameters");
|
res.writeStatus("400 Unauthorized").end("missing state and nonce URL parameters");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const loginUri = await openIDClient.authorizationUrl(state as string, nonce as string);
|
const loginUri = await openIDClient.authorizationUrl(
|
||||||
|
state as string,
|
||||||
|
nonce as string,
|
||||||
|
playUri as string | undefined
|
||||||
|
);
|
||||||
res.writeStatus("302");
|
res.writeStatus("302");
|
||||||
res.writeHeader("Location", loginUri);
|
res.writeHeader("Location", loginUri);
|
||||||
return res.end();
|
return res.end();
|
||||||
|
@ -20,13 +20,14 @@ class OpenIDClient {
|
|||||||
return this.issuerPromise;
|
return this.issuerPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public authorizationUrl(state: string, nonce: string) {
|
public authorizationUrl(state: string, nonce: string, playUri?: string) {
|
||||||
return this.initClient().then((client) => {
|
return this.initClient().then((client) => {
|
||||||
return client.authorizationUrl({
|
return client.authorizationUrl({
|
||||||
scope: "openid email",
|
scope: "openid email",
|
||||||
prompt: "login",
|
prompt: "login",
|
||||||
state: state,
|
state: state,
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
|
playUri: playUri,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user