diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 390f2556..4cf2a8fb 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -20,7 +20,7 @@ jobs: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@1.1.1 + - uses: rlespinasse/github-slug-action@3.1.0 - name: "Build and push front image" uses: docker/build-push-action@v1 @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v2 # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@1.1.1 + - uses: rlespinasse/github-slug-action@3.1.0 - name: "Build and push back image" uses: docker/build-push-action@v1 @@ -66,7 +66,7 @@ jobs: uses: actions/checkout@v2 # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@1.1.1 + - uses: rlespinasse/github-slug-action@3.1.0 - name: "Build and push back image" uses: docker/build-push-action@v1 @@ -90,7 +90,7 @@ jobs: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@1.1.1 + - uses: rlespinasse/github-slug-action@3.1.0 - name: "Build and push front image" uses: docker/build-push-action@v1 @@ -114,7 +114,7 @@ jobs: uses: actions/checkout@v2 # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@1.1.0 + - uses: rlespinasse/github-slug-action@3.1.0 - name: Deploy uses: thecodingmachine/deeployer@master diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index c71edd86..f037a99d 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -20,9 +20,9 @@ import {parse} from "query-string"; import {jwtTokenManager} from "../Services/JWTTokenManager"; import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; import {SocketManager, socketManager} from "../Services/SocketManager"; -import {emitInBatch, pongMaxInterval, refresLogoutTimerOnPong, resetPing} from "../Services/IoSocketHelpers"; +import {emitInBatch} from "../Services/IoSocketHelpers"; import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; +import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; export class IoSocketController { private nextUserId: number = 1; @@ -43,6 +43,7 @@ export class IoSocketController { if (token !== ADMIN_API_TOKEN) { console.log('Admin access refused for token: '+token) res.writeStatus("401 Unauthorized").end('Incorrect token'); + return; } const roomId = query.roomId as string; @@ -110,6 +111,7 @@ export class IoSocketController { this.app.ws('/room', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, + idleTimeout: SOCKET_IDLE_TIMER, maxPayloadLength: 16 * 1024 * 1024, maxBackpressure: 65536, // Maximum 64kB of data in the buffer. //idleTimeout: 10, @@ -239,8 +241,6 @@ export class IoSocketController { // Let's join the room const client = this.initClient(ws); //todo: into the upgrade instead? socketManager.handleJoinRoom(client); - resetPing(client); - refresLogoutTimerOnPong(ws as ExSocketInterface); //get data information and show messages if (ADMIN_API_URL) { @@ -293,9 +293,6 @@ export class IoSocketController { drain: (ws) => { console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); }, - pong(ws) { - refresLogoutTimerOnPong(ws as ExSocketInterface); - }, close: (ws, code, message) => { const Client = (ws as ExSocketInterface); try { diff --git a/back/src/Controller/MapController.ts b/back/src/Controller/MapController.ts index abe34886..c111ba64 100644 --- a/back/src/Controller/MapController.ts +++ b/back/src/Controller/MapController.ts @@ -1,11 +1,9 @@ -import {OK} from "http-status-codes"; -import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; import {parse} from "query-string"; import {adminApi} from "../Services/AdminApi"; -//todo: delete this + export class MapController extends BaseController{ constructor(private App : TemplatedApp) { @@ -36,18 +34,21 @@ export class MapController extends BaseController{ res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected organizationSlug parameter"); + return; } if (typeof query.worldSlug !== 'string') { console.error('Expected worldSlug parameter'); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected worldSlug parameter"); + return; } if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { console.error('Expected only one roomSlug parameter'); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected only one roomSlug parameter"); + return; } (async () => { diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 55fd1bb7..3a2ac99e 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -10,6 +10,7 @@ const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_ISS = process.env.JITSI_ISS || ''; const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; +export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export { SECRET_KEY, diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index e3d19138..c64a4952 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -25,8 +25,6 @@ export interface ExSocketInterface extends WebSocket, Identificable { emitInBatch: (payload: SubMessage) => void; batchedMessages: BatchMessage; batchTimeout: NodeJS.Timeout|null; - pingTimeout: NodeJS.Timeout|null; - pongTimeout: NodeJS.Timeout|null; disconnecting: boolean, tags: string[], textures: CharacterTexture[], diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index f8af822b..80712856 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -6,8 +6,13 @@ class GaugeManager { private nbClientsPerRoomGauge: Gauge; private nbGroupsPerRoomGauge: Gauge; private nbGroupsPerRoomCounter: Counter; + private nbRoomsGauge: Gauge; constructor() { + this.nbRoomsGauge = new Gauge({ + name: 'workadventure_nb_rooms', + help: 'Number of active rooms' + }); this.nbClientsGauge = new Gauge({ name: 'workadventure_nb_sockets', help: 'Number of connected sockets', @@ -31,6 +36,13 @@ class GaugeManager { }); } + incNbRoomGauge(): void { + this.nbRoomsGauge.inc(); + } + decNbRoomGauge(): void { + this.nbRoomsGauge.dec(); + } + incNbClientPerRoomGauge(roomId: string): void { this.nbClientsGauge.inc(); this.nbClientsPerRoomGauge.inc({ room: roomId }); diff --git a/back/src/Services/IoSocketHelpers.ts b/back/src/Services/IoSocketHelpers.ts index acaa0bb9..9c27c59a 100644 --- a/back/src/Services/IoSocketHelpers.ts +++ b/back/src/Services/IoSocketHelpers.ts @@ -18,22 +18,6 @@ export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): voi socket.batchTimeout = null; }, 100); } - - // If we send a message, we don't need to keep the connection alive - resetPing(socket); -} - -export function resetPing(ws: ExSocketInterface): void { - if (ws.pingTimeout) { - clearTimeout(ws.pingTimeout); - } - ws.pingTimeout = setTimeout(() => { - if (ws.disconnecting) { - return; - } - ws.ping(); - resetPing(ws); - }, 29000); } export function emitError(Client: ExSocketInterface, message: string): void { @@ -49,11 +33,3 @@ export function emitError(Client: ExSocketInterface, message: string): void { console.warn(message); } -export const pongMaxInterval = 30000; // the maximum duration (in ms) between pongs before we shutdown the connexion. - -export function refresLogoutTimerOnPong(ws: ExSocketInterface): void { - if(ws.pongTimeout) clearTimeout(ws.pongTimeout); - ws.pongTimeout = setTimeout(() => { - ws.close(); - }, pongMaxInterval); -} diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 4bd26778..97f008c4 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -351,6 +351,7 @@ export class SocketManager { world.leave(Client); if (world.isEmpty()) { this.Worlds.delete(Client.roomId); + gaugeManager.decNbRoomGauge(); } } //user leave previous room @@ -383,6 +384,7 @@ export class SocketManager { world.tags = data.tags world.policyType = Number(data.policy_type) } + gaugeManager.incNbRoomGauge(); this.Worlds.set(roomId, world); } return Promise.resolve(world) diff --git a/deeployer.libsonnet b/deeployer.libsonnet index db86fbb4..bad951f1 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -33,6 +33,7 @@ "ports": [80], "env": { "API_URL": "api."+url, + "ADMIN_URL": "admin."+url, "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", diff --git a/docker-compose.yaml b/docker-compose.yaml index 482dfbcb..7f21b16d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,6 +27,7 @@ services: HOST: "0.0.0.0" NODE_ENV: development API_URL: api.workadventure.localhost + ADMIN_URL: admin.workadventure.localhost STARTUP_COMMAND_1: yarn install TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443" TURN_USER: workadventure diff --git a/front/dist/resources/logos/boy.svg b/front/dist/resources/logos/boy.svg new file mode 100644 index 00000000..d6d9582e --- /dev/null +++ b/front/dist/resources/logos/boy.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/discussion.svg b/front/dist/resources/logos/discussion.svg new file mode 100644 index 00000000..1b4572b3 --- /dev/null +++ b/front/dist/resources/logos/discussion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 9aa9bc32..5fb43e07 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -1,6 +1,14 @@ +*{ + font-family: 'Open Sans', sans-serif; +} body{ overflow: hidden; } +body button:focus, +body img:focus, +body input:focus { + outline: -webkit-focus-ring-color auto 0; +} body .message-info{ width: 20%; height: auto; @@ -72,14 +80,16 @@ body .message-info.warning{ #div-myCamVideo { position: absolute; - right: 0; - bottom: 0; + right: 15px; + bottom: 15px; + border-radius: 15px 15px 15px 15px; } video#myCamVideo{ width: 15vw; -webkit-transform: scaleX(-1); transform: scaleX(-1); + border-radius: 15px 15px 15px 15px; /*width: 200px;*/ /*height: 113px;*/ } @@ -372,6 +382,7 @@ body { margin: 2%; transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s, max-width 0.2s; cursor: pointer; + border-radius: 15px 15px 15px 15px; } .sidebar > div:hover { @@ -384,6 +395,7 @@ body { justify-content: center; flex-direction: column; overflow: hidden; + border-radius: 15px; } .chat-mode { @@ -435,7 +447,7 @@ body { max-height: 80%; top: -80%; left: 10%; - background: #000000a6; + background: #333333; z-index: 200; transition: all 0.1s ease-out; } @@ -532,7 +544,7 @@ body { border: 1px solid black; background-color: #00000000; color: #ffda01; - border-radius: 10px; + border-radius: 15px; padding: 10px 30px; transition: all .2s ease; } @@ -627,6 +639,7 @@ div.modal-report-user{ left: calc(50% - 400px); top: 100px; background-color: #000000ad; + border-radius: 15px; } .modal-report-user textarea{ @@ -638,6 +651,7 @@ div.modal-report-user{ color: white; width: calc(100% - 60px); margin: 30px; + border-radius: 15px; } .modal-report-user img{ @@ -669,7 +683,7 @@ div.modal-report-user{ border: 1px solid black; background-color: #00000000; color: #ffda01; - border-radius: 10px; + border-radius: 15px; padding: 10px 30px; transition: all .2s ease; } @@ -701,6 +715,193 @@ div.modal-report-user{ max-width: calc(800px - 60px); /* size of modal - padding*/ } +/*MESSAGE*/ +.discussion{ + position: fixed; + left: -300px; + top: 0px; + width: 220px; + height: 100%; + background-color: #333333; + padding: 20px; + transition: all 0.5s ease; +} +.discussion.active{ + left: 0; +} +.discussion .active-btn{ + display: none; + cursor: pointer; + height: 50px; + width: 50px; + background-color: #2d2d2dba; + position: absolute; + top: calc(50% - 25px); + margin-left: 315px; + border-radius: 50%; + border: none; + transition: all 0.5s ease; +} +.discussion .active-btn.active{ + display: block; +} +.discussion .active-btn:hover { + transform: scale(1.1) rotateY(3.142rad); +} +.discussion .active-btn img{ + width: 26px; + height: 26px; + margin: 13px 5px; +} + +.discussion .close-btn{ + position: absolute; + top: 0; + right: 10px; + background: none; + border: none; + cursor: pointer; +} +.discussion .close-btn img{ + height: 15px; + right: 15px; +} + +.discussion p{ + color: white; + font-size: 22px; + padding-left: 10px; + margin: 0; +} + +.discussion .participants{ + height: 200px; + margin: 10px 0; +} + +.discussion .participants .participant{ + display: flex; + margin: 5px 10px; + background-color: #ffffff69; + padding: 5px; + border-radius: 15px; + cursor: pointer; +} + +.discussion .participants .participant:hover{ + background-color: #ffffff; +} +.discussion .participants .participant:hover p{ + color: black; +} + +.discussion .participants .participant:before { + content: ''; + height: 10px; + width: 10px; + background-color: #1e7e34; + position: absolute; + margin-left: 18px; + border-radius: 50%; + margin-top: 18px; +} + +.discussion .participants .participant img{ + width: 26px; + height: 26px; +} + +.discussion .participants .participant p{ + font-size: 16px; + margin-left: 10px; + margin-top: 2px; +} + +.discussion .participants .participant button.report-btn{ + cursor: pointer; + position: absolute; + background-color: #2d2d2dba; + right: 34px; + margin: 0px; + padding: 6px 0px; + border-radius: 15px; + border: none; + color: white; + width: 0px; + overflow: hidden; + transition: all .5s ease; +} + +.discussion .participants .participant:hover button.report-btn{ + width: 70px; +} + +.discussion .messages{ + position: absolute; + height: calc(100% - 360px); + overflow-x: hidden; + overflow-y: auto; + max-width: calc(100% - 40px); + width: calc(100% - 40px); +} + +.discussion .messages h2{ + color: white; +} + +.discussion .messages .message{ + margin: 5px; + float: right; + text-align: right; + width: 100%; +} + +.discussion .messages .message.me{ + float: left; + text-align: left; +} + +.discussion .messages .message p{ + font-size: 12px; +} + +.discussion .messages .message p.body{ + font-size: 16px; + overflow: hidden; + white-space: pre-wrap; + word-wrap: break-word; +} + +.discussion .send-message{ + position: absolute; + bottom: 45px; + width: 220px; + height: 26px; +} + +.discussion .send-message input{ + position: absolute; + width: calc(100% - 10px); + height: 20px; + background-color: #171717; + color: white; + border-radius: 15px; + border: none; + padding: 6px; +} + +.discussion .send-message img{ + position: absolute; + margin-right: 10px; + width: 20px; + height: 20px; + background-color: #ffffff69; +} +.discussion .send-message img:hover{ + background-color: #ffffff; +} + + /** Action button **/ div.action{ position: absolute; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 19d011ff..b25e2d76 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -26,6 +26,7 @@ import { QueryJitsiJwtMessage, SendJitsiJwtMessage, CharacterLayerMessage, + PingMessage, SendUserMessage } from "../Messages/generated/messages_pb" @@ -42,6 +43,8 @@ import { } from "./ConnexionModels"; import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character"; +const manualPingDelay = 20000; + export class RoomConnection implements RoomConnection { private readonly socket: WebSocket; private userId: number|null = null; @@ -84,7 +87,9 @@ export class RoomConnection implements RoomConnection { this.socket.binaryType = 'arraybuffer'; this.socket.onopen = (ev) => { - //console.log('WS connected'); + //we manually ping every 20s to not be logged out by the server, even when the game is in background. + const pingMessage = new PingMessage(); + setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); }; this.socket.onmessage = (messageEvent) => { diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 60f9cd3b..d3b6c809 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,12 +1,12 @@ const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "api.workadventure.localhost"); -const ADMIN_URL = API_URL.replace('api', 'admin'); +const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "admin.workadventure.localhost"); const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca"; const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com'; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$'; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; -const RESOLUTION = 3; +const RESOLUTION = 2; const ZOOM_LEVEL = 1/*3/4*/; const POSITION_DELAY = 200; // Wait 200ms between sending position events const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 3e9361ff..6e85e6cf 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -342,51 +342,17 @@ export class GameScene extends ResizableScene implements CenterListener { throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.'); } - // If there is an init position passed - if (this.initPosition !== null) { - this.startX = this.initPosition.x; - this.startY = this.initPosition.y; - } else { - // Now, let's find the start layer - if (this.room.hash) { - for (const layer of this.mapFile.layers) { - if (this.room.hash === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) { - const startPosition = this.startUser(layer); - this.startX = startPosition.x; - this.startY = startPosition.y; - } - } - } - if (this.startX === undefined) { - // If we have no start layer specified or if the hash passed does not exist, let's go with the default start position. - for (const layer of this.mapFile.layers) { - if (layer.type === 'tilelayer' && layer.name === "start") { - const startPosition = this.startUser(layer); - this.startX = startPosition.x; - this.startY = startPosition.y; - } - } - } - } - // Still no start position? Something is wrong with the map, we need a "start" layer. - if (this.startX === undefined) { - console.warn('This map is missing a layer named "start" that contains the available default start positions.'); - // Let's start in the middle of the map - this.startX = this.mapFile.width * 16; - this.startY = this.mapFile.height * 16; - } + this.initStartXAndStartY(); //add entities this.Objects = new Array(); - //init event click - this.EventToClickOnTile(); - //initialise list of other player this.MapPlayers = this.physics.add.group({immovable: true}); //create input to move this.userInputManager = new UserInputManager(this); + mediaManager.setUserInputManager(this.userInputManager); //notify game manager can to create currentUser in map this.createCurrentPlayer(); @@ -466,62 +432,7 @@ export class GameScene extends ResizableScene implements CenterListener { // From now, this game scene will be notified of reposition events layoutManager.setListener(this); - - this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => { - if (newValue === undefined) { - layoutManager.removeActionButton('openWebsite', this.userInputManager); - coWebsiteManager.closeCoWebsite(); - }else{ - const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string); - layoutManager.removeActionButton('openWebsite', this.userInputManager); - }; - - const openWebsiteTriggerValue = allProps.get('openWebsiteTrigger'); - if(openWebsiteTriggerValue && openWebsiteTriggerValue === 'onaction') { - layoutManager.addActionButton('openWebsite', 'Click on SPACE to open web site', () => { - openWebsiteFunction(); - }, this.userInputManager); - }else{ - openWebsiteFunction(); - } - } - }); - - this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { - if (newValue === undefined) { - layoutManager.removeActionButton('jitsiRoom', this.userInputManager); - this.stopJitsi(); - }else{ - const openJitsiRoomFunction = () => { - if (JITSI_PRIVATE_MODE) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; - - this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag); - } else { - this.startJitsi(newValue as string); - } - layoutManager.removeActionButton('jitsiRoom', this.userInputManager); - } - - const jitsiTriggerValue = allProps.get('jitsiTrigger'); - if(jitsiTriggerValue && jitsiTriggerValue === 'onaction') { - layoutManager.addActionButton('jitsiRoom', 'Click on SPACE to enter in jitsi meet room', () => { - openJitsiRoomFunction(); - }, this.userInputManager); - }else{ - openJitsiRoomFunction(); - } - } - }); - - this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { - if (newValue === undefined || newValue === false || newValue === '') { - this.connection.setSilent(false); - } else { - this.connection.setSilent(true); - } - }); + this.triggerOnMapLayerPropertyChange(); const camera = this.cameras.main; @@ -629,7 +540,7 @@ export class GameScene extends ResizableScene implements CenterListener { }); // When connection is performed, let's connect SimplePeer - this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic); + this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.GameManager.getPlayerName()); this.GlobalMessageManager = new GlobalMessageManager(this.connection); this.UserMessageManager = new UserMessageManager(this.connection); @@ -654,15 +565,77 @@ export class GameScene extends ResizableScene implements CenterListener { this.gameMap.setPosition(event.x, event.y); }) - this.scene.wake(); this.scene.sleep(ReconnectingSceneName); + //init user position and play trigger to check layers properties + this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); + return connection; }); } + private triggerOnMapLayerPropertyChange(){ + this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => { + if (newValue === undefined) { + layoutManager.removeActionButton('openWebsite', this.userInputManager); + coWebsiteManager.closeCoWebsite(); + }else{ + const openWebsiteFunction = () => { + coWebsiteManager.loadCoWebsite(newValue as string); + layoutManager.removeActionButton('openWebsite', this.userInputManager); + }; + + const openWebsiteTriggerValue = allProps.get('openWebsiteTrigger'); + if(openWebsiteTriggerValue && openWebsiteTriggerValue === 'onaction') { + layoutManager.addActionButton('openWebsite', 'Click on SPACE to open web site', () => { + openWebsiteFunction(); + }, this.userInputManager); + }else{ + openWebsiteFunction(); + } + } + }); + this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { + if (newValue === undefined) { + layoutManager.removeActionButton('jitsiRoom', this.userInputManager); + this.stopJitsi(); + }else{ + const openJitsiRoomFunction = () => { + if (JITSI_PRIVATE_MODE) { + const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + + this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag); + } else { + this.startJitsi(newValue as string); + } + layoutManager.removeActionButton('jitsiRoom', this.userInputManager); + } + + const jitsiTriggerValue = allProps.get('jitsiTrigger'); + if(jitsiTriggerValue && jitsiTriggerValue === 'onaction') { + layoutManager.addActionButton('jitsiRoom', 'Click on SPACE to enter in jitsi meet room', () => { + openJitsiRoomFunction(); + }, this.userInputManager); + }else{ + openJitsiRoomFunction(); + } + } + }) + this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === '') { + this.connection.setSilent(false); + } else { + this.connection.setSilent(true); + } + }); + } + private switchLayoutMode(): void { + //if discussion is activated, this layout cannot be activated + if(mediaManager.activatedDiscussion){ + return; + } const mode = layoutManager.getLayoutMode(); if (mode === LayoutMode.Presentation) { layoutManager.switchLayoutMode(LayoutMode.VideoChat); @@ -675,6 +648,40 @@ export class GameScene extends ResizableScene implements CenterListener { } } + private initStartXAndStartY() { + // If there is an init position passed + if (this.initPosition !== null) { + this.startX = this.initPosition.x; + this.startY = this.initPosition.y; + } else { + // Now, let's find the start layer + if (this.room.hash) { + this.initPositionFromLayerName(this.room.hash); + } + if (this.startX === undefined) { + // If we have no start layer specified or if the hash passed does not exist, let's go with the default start position. + this.initPositionFromLayerName("start"); + } + } + // Still no start position? Something is wrong with the map, we need a "start" layer. + if (this.startX === undefined) { + console.warn('This map is missing a layer named "start" that contains the available default start positions.'); + // Let's start in the middle of the map + this.startX = this.mapFile.width * 16; + this.startY = this.mapFile.height * 16; + } + } + + private initPositionFromLayerName(layerName: string) { + for (const layer of this.mapFile.layers) { + if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === "start" || this.isStartLayer(layer))) { + const startPosition = this.startUser(layer); + this.startX = startPosition.x; + this.startY = startPosition.y; + } + } + } + private getExitUrl(layer: ITiledMapLayer): string|undefined { return this.getProperty(layer, "exitUrl") as string|undefined; } @@ -713,9 +720,6 @@ export class GameScene extends ResizableScene implements CenterListener { instance = this.instance; } - //console.log('existSceneUrl', exitSceneUrl); - //console.log('existSceneInstance', instance); - const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3); const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol; @@ -733,13 +737,9 @@ export class GameScene extends ResizableScene implements CenterListener { this.loadNextGame(layer, mapWidth, fullPath); } - /** - * - * @param layer - * @param mapWidth - */ //todo: push that into the gameManager private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ + const room = new Room(roomId); gameManager.loadMap(room, this.scene); const exitSceneKey = roomId; @@ -772,9 +772,6 @@ export class GameScene extends ResizableScene implements CenterListener { } } - /** - * @param layer - */ private startUser(layer: ITiledMapLayer): PositionInterface { const tiles = layer.data; if (typeof(tiles) === 'string') { @@ -930,17 +927,6 @@ export class GameScene extends ResizableScene implements CenterListener { }); } - EventToClickOnTile(){ - // debug code to get a tile properties by clicking on it - /*this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{ - //pixel position toz tile position - const tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY)); - if(tile){ - this.CurrentPlayer.say("Your touch " + tile.layer.name); - } - });*/ - } - /** * @param time * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. @@ -985,13 +971,19 @@ export class GameScene extends ResizableScene implements CenterListener { }); const nextSceneKey = this.checkToExit(); - if (nextSceneKey) { + if (!nextSceneKey) return; + if (nextSceneKey.key !== this.scene.key) { // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. this.connection.closeConnection(); this.simplePeer.unregister(); this.scene.stop(); this.scene.remove(this.scene.key); this.scene.start(nextSceneKey.key); + } else { + //if the exit points to the current map, we simply teleport the user back to the startLayer + this.initPositionFromLayerName(this.room.hash || 'start'); + this.CurrentPlayer.x = this.startX; + this.CurrentPlayer.y = this.startY; } } @@ -1093,11 +1085,6 @@ export class GameScene extends ResizableScene implements CenterListener { await Promise.all(loadPromises); player.addTextures(characterLayerList, 1); - //init collision - /*this.physics.add.collider(this.CurrentPlayer, player, (CurrentPlayer: CurrentGamerInterface, MapPlayer: GamerInterface) => { - CurrentPlayer.say("Hello, how are you ? "); - });*/ - } /** @@ -1140,7 +1127,6 @@ export class GameScene extends ResizableScene implements CenterListener { // We do not update the player position directly (because it is sent only every 200ms). // Instead we use the PlayersPositionInterpolator that will do a smooth animation over the next 200ms. const playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); - //console.log('Target position: ', player.x, player.y); this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); } @@ -1189,11 +1175,6 @@ export class GameScene extends ResizableScene implements CenterListener { /** * Sends to the server an event emitted by one of the ActionableItems. - * - * @param itemId - * @param eventName - * @param state - * @param parameters */ emitActionableEvent(itemId: number, eventName: string, state: unknown, parameters: unknown) { this.connection.emitActionableEvent(itemId, eventName, state, parameters); diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 32037858..56502704 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -7,6 +7,7 @@ import {mediaManager} from "../../WebRtc/MediaManager"; import {RESOLUTION} from "../../Enum/EnvironmentVariable"; import {SoundMeter} from "../Components/SoundMeter"; import {SoundMeterSprite} from "../Components/SoundMeterSprite"; +import {HtmlUtils} from "../../WebRtc/HtmlUtils"; export const EnableCameraSceneName = "EnableCameraScene"; enum LoginTextures { @@ -93,7 +94,7 @@ export class EnableCameraScene extends Phaser.Scene { this.login(); }); - this.getElementByIdOrFail('webRtcSetup').classList.add('active'); + HtmlUtils.getElementByIdOrFail('webRtcSetup').classList.add('active'); const mediaPromise = mediaManager.getCamera(); mediaPromise.then(this.getDevices.bind(this)); @@ -150,10 +151,10 @@ export class EnableCameraScene extends Phaser.Scene { * Function called each time a camera is changed */ private setupStream(stream: MediaStream): void { - const img = this.getElementByIdOrFail('webRtcSetupNoVideo'); + const img = HtmlUtils.getElementByIdOrFail('webRtcSetupNoVideo'); img.style.display = 'none'; - const div = this.getElementByIdOrFail('myCamVideoSetup'); + const div = HtmlUtils.getElementByIdOrFail('myCamVideoSetup'); div.srcObject = stream; this.soundMeter.connectToSource(stream, new window.AudioContext()); @@ -164,7 +165,7 @@ export class EnableCameraScene extends Phaser.Scene { private updateWebCamName(): void { if (this.camerasList.length > 1) { - const div = this.getElementByIdOrFail('myCamVideoSetup'); + const div = HtmlUtils.getElementByIdOrFail('myCamVideoSetup'); let label = this.camerasList[this.cameraSelected].label; // remove text in parenthesis @@ -211,10 +212,10 @@ export class EnableCameraScene extends Phaser.Scene { } private reposition(): void { - let div = this.getElementByIdOrFail('myCamVideoSetup'); + let div = HtmlUtils.getElementByIdOrFail('myCamVideoSetup'); let bounds = div.getBoundingClientRect(); if (!div.srcObject) { - div = this.getElementByIdOrFail('webRtcSetup'); + div = HtmlUtils.getElementByIdOrFail('webRtcSetup'); bounds = div.getBoundingClientRect(); } @@ -255,7 +256,7 @@ export class EnableCameraScene extends Phaser.Scene { } private login(): void { - this.getElementByIdOrFail('webRtcSetup').style.display = 'none'; + HtmlUtils.getElementByIdOrFail('webRtcSetup').style.display = 'none'; this.soundMeter.stop(); window.removeEventListener('resize', this.repositionCallback); @@ -276,13 +277,4 @@ export class EnableCameraScene extends Phaser.Scene { } this.updateWebCamName(); } - - private getElementByIdOrFail(id: string): T { - const elem = document.getElementById(id); - if (elem === null) { - throw new Error("Cannot find HTML element with id '"+id+"'"); - } - // FIXME: does not check the type of the returned type - return elem as T; - } } diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index 9f4a6071..c8d91609 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -40,7 +40,9 @@ export class UserInputManager { initKeyBoardEvent(){ this.KeysCode = [ {event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) }, + {event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) }, {event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) }, + {event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) }, {event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) }, {event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) }, diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 46d03702..b625bf6e 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -16,31 +16,35 @@ class CoWebsiteManager { private opened: iframeStates = iframeStates.closed; private observers = new Array(); + /** + * Quickly going in and out of an iframe trigger can create conflicts between the iframe states. + * So we use this promise to queue up every cowebsite state transition + */ + private currentOperationPromise: Promise = Promise.resolve(); + private cowebsiteDiv: HTMLDivElement; - private close(): HTMLDivElement { - const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); - cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition - cowebsiteDiv.classList.add('hidden'); + constructor() { + this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); + } + + private close(): void { + this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition + this.cowebsiteDiv.classList.add('hidden'); this.opened = iframeStates.closed; - return cowebsiteDiv; } - private load(): HTMLDivElement { - const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); - cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition - cowebsiteDiv.classList.add('loading'); + private load(): void { + this.cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition + this.cowebsiteDiv.classList.add('loading'); this.opened = iframeStates.loading; - return cowebsiteDiv; } - private open(): HTMLDivElement { - const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); - cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition + private open(): void { + this.cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition this.opened = iframeStates.opened; - return cowebsiteDiv; } public loadCoWebsite(url: string): void { - const cowebsiteDiv = this.load(); - cowebsiteDiv.innerHTML = ''; + this.load(); + this.cowebsiteDiv.innerHTML = ''; const iframe = document.createElement('iframe'); iframe.id = 'cowebsite-iframe'; @@ -48,11 +52,11 @@ class CoWebsiteManager { const onloadPromise = new Promise((resolve) => { iframe.onload = () => resolve(); }); - cowebsiteDiv.appendChild(iframe); + this.cowebsiteDiv.appendChild(iframe); const onTimeoutPromise = new Promise((resolve) => { setTimeout(() => resolve(), 2000); }); - Promise.race([onloadPromise, onTimeoutPromise]).then(() => { + this.currentOperationPromise = this.currentOperationPromise.then(() =>Promise.race([onloadPromise, onTimeoutPromise])).then(() => { this.open(); setTimeout(() => { this.fire(); @@ -64,24 +68,26 @@ class CoWebsiteManager { * Just like loadCoWebsite but the div can be filled by the user. */ public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise): void { - const cowebsiteDiv = this.load(); - callback(cowebsiteDiv).then(() => { + this.load(); + this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteDiv)).then(() => { this.open(); setTimeout(() => { this.fire(); - }, animationTime) + }, animationTime); }).catch(() => this.closeCoWebsite()); } public closeCoWebsite(): Promise { - return new Promise((resolve, reject) => { - const cowebsiteDiv = this.close(); + this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => { + if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example + this.close(); this.fire(); setTimeout(() => { + this.cowebsiteDiv.innerHTML = ''; resolve(); - setTimeout(() => cowebsiteDiv.innerHTML = '', 500) }, animationTime) - }); + })); + return this.currentOperationPromise; } public getGameSize(): {width: number, height: number} { @@ -104,6 +110,7 @@ class CoWebsiteManager { } } + //todo: is it still useful to allow any kind of observers? public onStateChange(observer: CoWebsiteStateChangedCallback) { this.observers.push(observer); } diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts new file mode 100644 index 00000000..097bf3a3 --- /dev/null +++ b/front/src/WebRtc/DiscussionManager.ts @@ -0,0 +1,231 @@ +import {HtmlUtils} from "./HtmlUtils"; +import {MediaManager, ReportCallback, UpdatedLocalStreamCallback} from "./MediaManager"; +import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; +export type SendMessageCallback = (message:string) => void; + +export class DiscussionManager { + private mainContainer: HTMLDivElement; + + private divDiscuss?: HTMLDivElement; + private divParticipants?: HTMLDivElement; + private nbpParticipants?: HTMLParagraphElement; + private divMessages?: HTMLParagraphElement; + private buttonActiveDiscussion?: HTMLButtonElement; + + private participants: Map = new Map(); + + private activeDiscussion: boolean = false; + + private sendMessageCallBack : Map = new Map(); + + private userInputManager?: UserInputManager; + + constructor(private mediaManager: MediaManager, name: string) { + this.mainContainer = HtmlUtils.getElementByIdOrFail('main-container'); + this.createDiscussPart(name); + } + + private createDiscussPart(name: string) { + this.divDiscuss = document.createElement('div'); + this.divDiscuss.classList.add('discussion'); + + const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button'); + this.buttonActiveDiscussion = document.createElement('button'); + buttonCloseDiscussion.classList.add('close-btn'); + buttonCloseDiscussion.innerHTML = ``; + buttonCloseDiscussion.addEventListener('click', () => { + this.hideDiscussion(); + this.showButtonDiscussionBtn(); + }); + this.buttonActiveDiscussion.classList.add('active-btn'); + this.buttonActiveDiscussion.innerHTML = ``; + this.buttonActiveDiscussion.addEventListener('click', () => { + this.showDiscussionPart(); + }); + this.divDiscuss.appendChild(buttonCloseDiscussion); + this.divDiscuss.appendChild(this.buttonActiveDiscussion); + + const myName: HTMLParagraphElement = document.createElement('p'); + myName.innerText = name.toUpperCase(); + this.nbpParticipants = document.createElement('p'); + this.nbpParticipants.innerText = 'PARTICIPANTS (1)'; + + this.divParticipants = document.createElement('div'); + this.divParticipants.classList.add('participants'); + + this.divMessages = document.createElement('div'); + this.divMessages.classList.add('messages'); + this.divMessages.innerHTML = "

Local messages

" + + this.divDiscuss.appendChild(myName); + this.divDiscuss.appendChild(this.nbpParticipants); + this.divDiscuss.appendChild(this.divParticipants); + this.divDiscuss.appendChild(this.divMessages); + + const sendDivMessage: HTMLDivElement = document.createElement('div'); + sendDivMessage.classList.add('send-message'); + const inputMessage: HTMLInputElement = document.createElement('input'); + inputMessage.type = "text"; + inputMessage.addEventListener('keyup', (event: KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + if(inputMessage.value === null + || inputMessage.value === '' + || inputMessage.value === undefined) { + return; + } + this.addMessage(name, inputMessage.value, true); + for(const callback of this.sendMessageCallBack.values()) { + callback(inputMessage.value); + } + inputMessage.value = ""; + } + }); + sendDivMessage.appendChild(inputMessage); + this.divDiscuss.appendChild(sendDivMessage); + + //append in main container + this.mainContainer.appendChild(this.divDiscuss); + + this.addParticipant('me', 'Moi', undefined, true); + } + + public addParticipant( + userId: number|string, + name: string|undefined, + img?: string|undefined, + isMe: boolean = false, + reportCallback?: ReportCallback + ) { + const divParticipant: HTMLDivElement = document.createElement('div'); + divParticipant.classList.add('participant'); + divParticipant.id = `participant-${userId}`; + + const divImgParticipant: HTMLImageElement = document.createElement('img'); + divImgParticipant.src = 'resources/logos/boy.svg'; + if (img !== undefined) { + divImgParticipant.src = img; + } + const divPParticipant: HTMLParagraphElement = document.createElement('p'); + if(!name){ + name = 'Anonymous'; + } + divPParticipant.innerText = name; + + divParticipant.appendChild(divImgParticipant); + divParticipant.appendChild(divPParticipant); + + if(!isMe) { + const reportBanUserAction: HTMLButtonElement = document.createElement('button'); + reportBanUserAction.classList.add('report-btn') + reportBanUserAction.innerText = 'Report'; + reportBanUserAction.addEventListener('click', () => { + if(reportCallback) { + this.mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback); + }else{ + console.info('report feature is not activated!'); + } + }); + divParticipant.appendChild(reportBanUserAction); + } + + this.divParticipants?.appendChild(divParticipant); + + this.participants.set(userId, divParticipant); + this.showButtonDiscussionBtn(); + + this.updateParticipant(this.participants.size); + } + + public updateParticipant(nb: number) { + if (!this.nbpParticipants) { + return; + } + this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`; + } + + public addMessage(name: string, message: string, isMe: boolean = false) { + const divMessage: HTMLDivElement = document.createElement('div'); + divMessage.classList.add('message'); + if(isMe){ + divMessage.classList.add('me'); + } + + const pMessage: HTMLParagraphElement = document.createElement('p'); + const date = new Date(); + if(isMe){ + name = 'Moi'; + } + pMessage.innerHTML = `${name} + + ${date.getHours()}:${date.getMinutes()} + `; + divMessage.appendChild(pMessage); + + const userMessage: HTMLParagraphElement = document.createElement('p'); + userMessage.innerText = message; + userMessage.classList.add('body'); + divMessage.appendChild(userMessage); + + this.divMessages?.appendChild(divMessage); + } + + public removeParticipant(userId: number|string){ + const element = this.participants.get(userId); + if(element){ + element.remove(); + this.participants.delete(userId); + } + //if all participant leave, hide discussion button + if(this.participants.size === 1){ + this.hideButtonDiscussionBtn(); + } + + this.sendMessageCallBack.delete(userId); + } + + public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void { + this.sendMessageCallBack.set(userId, callback); + } + + get activatedDiscussion(){ + return this.activeDiscussion; + } + + private showButtonDiscussionBtn(){ + //if it's first participant, show discussion button + if(this.activatedDiscussion || this.participants.size === 1) { + return; + } + this.buttonActiveDiscussion?.classList.add('active'); + } + + private showDiscussion(){ + this.activeDiscussion = true; + if(this.userInputManager) { + this.userInputManager.clearAllInputKeyboard(); + } + this.divDiscuss?.classList.add('active'); + } + + private hideDiscussion(){ + this.activeDiscussion = false; + if(this.userInputManager) { + this.userInputManager.initKeyBoardEvent(); + } + this.divDiscuss?.classList.remove('active'); + } + + private hideButtonDiscussionBtn(){ + this.buttonActiveDiscussion?.classList.remove('active'); + } + + public setUserInputManager(userInputManager : UserInputManager){ + this.userInputManager = userInputManager; + } + + public showDiscussionPart(){ + this.showDiscussion(); + this.hideButtonDiscussionBtn(); + } +} \ No newline at end of file diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index 6ad69e2a..736b5244 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -50,8 +50,9 @@ class JitsiFactory { delete options.jwt; } - return new Promise((resolve) => { + return new Promise((resolve, reject) => { options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations. + setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); this.jitsiApi.executeCommand('displayName', playerName); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index ab6d9a3b..ce1878fb 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,5 +1,7 @@ import {DivImportance, layoutManager} from "./LayoutManager"; import {HtmlUtils} from "./HtmlUtils"; +import {DiscussionManager, SendMessageCallback} from "./DiscussionManager"; +import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any const videoConstraint: boolean|MediaTrackConstraints = { @@ -38,57 +40,65 @@ export class MediaManager { private cinemaBtn: HTMLDivElement; private monitorBtn: HTMLDivElement; + private discussionManager: DiscussionManager; + + private userInputManager?: UserInputManager; + + private hasCamera = true; + constructor() { - this.myCamVideo = this.getElementByIdOrFail('myCamVideo'); - this.webrtcInAudio = this.getElementByIdOrFail('audio-webrtc-in'); + this.myCamVideo = HtmlUtils.getElementByIdOrFail('myCamVideo'); + this.webrtcInAudio = HtmlUtils.getElementByIdOrFail('audio-webrtc-in'); this.webrtcInAudio.volume = 0.2; - this.microphoneBtn = this.getElementByIdOrFail('btn-micro'); - this.microphoneClose = this.getElementByIdOrFail('microphone-close'); + this.microphoneBtn = HtmlUtils.getElementByIdOrFail('btn-micro'); + this.microphoneClose = HtmlUtils.getElementByIdOrFail('microphone-close'); this.microphoneClose.style.display = "none"; this.microphoneClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enableMicrophone(); //update tracking }); - this.microphone = this.getElementByIdOrFail('microphone'); + this.microphone = HtmlUtils.getElementByIdOrFail('microphone'); this.microphone.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disableMicrophone(); //update tracking }); - this.cinemaBtn = this.getElementByIdOrFail('btn-video'); - this.cinemaClose = this.getElementByIdOrFail('cinema-close'); + this.cinemaBtn = HtmlUtils.getElementByIdOrFail('btn-video'); + this.cinemaClose = HtmlUtils.getElementByIdOrFail('cinema-close'); this.cinemaClose.style.display = "none"; this.cinemaClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enableCamera(); //update tracking }); - this.cinema = this.getElementByIdOrFail('cinema'); + this.cinema = HtmlUtils.getElementByIdOrFail('cinema'); this.cinema.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disableCamera(); //update tracking }); - this.monitorBtn = this.getElementByIdOrFail('btn-monitor'); - this.monitorClose = this.getElementByIdOrFail('monitor-close'); + this.monitorBtn = HtmlUtils.getElementByIdOrFail('btn-monitor'); + this.monitorClose = HtmlUtils.getElementByIdOrFail('monitor-close'); this.monitorClose.style.display = "block"; this.monitorClose.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.enableScreenSharing(); //update tracking }); - this.monitor = this.getElementByIdOrFail('monitor'); + this.monitor = HtmlUtils.getElementByIdOrFail('monitor'); this.monitor.style.display = "none"; this.monitor.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.disableScreenSharing(); //update tracking }); + + this.discussionManager = new DiscussionManager(this,''); } public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { @@ -126,16 +136,19 @@ export class MediaManager { } public showGameOverlay(){ - const gameOverlay = this.getElementByIdOrFail('game-overlay'); + const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); gameOverlay.classList.add('active'); } public hideGameOverlay(){ - const gameOverlay = this.getElementByIdOrFail('game-overlay'); + const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); gameOverlay.classList.remove('active'); } public enableCamera() { + if(!this.hasCamera){ + return; + } this.cinemaClose.style.display = "none"; this.cinemaBtn.classList.remove("disabled"); this.cinema.style.display = "block"; @@ -146,13 +159,7 @@ export class MediaManager { } public async disableCamera() { - this.cinemaClose.style.display = "block"; - this.cinema.style.display = "none"; - this.cinemaBtn.classList.add("disabled"); - this.constraintsMedia.video = false; - this.myCamVideo.srcObject = null; - this.stopCamera(); - + this.disabledCameraView(); if (this.constraintsMedia.audio !== false) { const stream = await this.getCamera(); this.triggerUpdatedLocalStreamCallbacks(stream); @@ -161,6 +168,15 @@ export class MediaManager { } } + private disabledCameraView(){ + this.cinemaClose.style.display = "block"; + this.cinema.style.display = "none"; + this.cinemaBtn.classList.add("disabled"); + this.constraintsMedia.video = false; + this.myCamVideo.srcObject = null; + this.stopCamera(); + } + public enableMicrophone() { this.microphoneClose.style.display = "none"; this.microphone.style.display = "block"; @@ -267,24 +283,33 @@ export class MediaManager { } } - try { - const stream = await navigator.mediaDevices.getUserMedia(this.constraintsMedia); + return this.getLocalStream().catch(() => { + console.info('Error get camera, trying with video option at null'); + this.disabledCameraView(); + return this.getLocalStream().then((stream : MediaStream) => { + this.hasCamera = false; + return stream; + }).catch((err) => { + console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err); + throw err; + }); + }); + //TODO resize remote cam + /*console.log(this.localStream.getTracks()); + let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video"); + let {width, height} = videoMediaStreamTrack.getSettings(); + console.info(`${width}x${height}`); // 6*/ + } + + private getLocalStream() : Promise { + return navigator.mediaDevices.getUserMedia(this.constraintsMedia).then((stream : MediaStream) => { this.localStream = stream; this.myCamVideo.srcObject = this.localStream; - return stream; - - //TODO resize remote cam - /*console.log(this.localStream.getTracks()); - let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video"); - let {width, height} = videoMediaStreamTrack.getSettings(); - console.info(`${width}x${height}`); // 6*/ - } catch (err) { - console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err); - this.localStream = null; + }).catch((err: Error) => { throw err; - } + }); } /** @@ -355,14 +380,17 @@ export class MediaManager { layoutManager.add(DivImportance.Normal, userId, html); if (reportCallBack) { - const reportBtn = this.getElementByIdOrFail(`report-${userId}`); + const reportBtn = HtmlUtils.getElementByIdOrFail(`report-${userId}`); reportBtn.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); this.showReportModal(userId, userName, reportCallBack); }); } - this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); + this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); + + //permit to create participant in discussion part + this.addNewParticipant(userId, userName, undefined, reportCallBack); } addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ @@ -376,7 +404,7 @@ export class MediaManager { layoutManager.add(divImportance, userId, html); - this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); + this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); } disabledMicrophoneByUserId(userId: number){ @@ -437,6 +465,9 @@ export class MediaManager { removeActiveVideo(userId: string){ layoutManager.remove(userId); this.remoteVideo.delete(userId); + + //permit to remove user in discussion part + this.removeParticipant(userId); } removeActiveScreenSharingVideo(userId: string) { this.removeActiveVideo(`screen-sharing-${userId}`) @@ -499,18 +530,9 @@ export class MediaManager { return color; } - private getElementByIdOrFail(id: string): T { - const elem = document.getElementById(id); - if (elem === null) { - throw new Error("Cannot find HTML element with id '"+id+"'"); - } - // FIXME: does not check the type of the returned type - return elem as T; - } - - private showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){ + public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){ //create report text area - const mainContainer = this.getElementByIdOrFail('main-container'); + const mainContainer = HtmlUtils.getElementByIdOrFail('main-container'); const divReport = document.createElement('div'); divReport.classList.add('modal-report-user'); @@ -565,7 +587,34 @@ export class MediaManager { mainContainer.appendChild(divReport); } + public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){ + this.discussionManager.addParticipant(userId, name, img, false, reportCallBack); + } + public removeParticipant(userId: number|string){ + this.discussionManager.removeParticipant(userId); + } + + public addNewMessage(name: string, message: string, isMe: boolean = false){ + this.discussionManager.addMessage(name, message, isMe); + + //when there are new message, show discussion + if(!this.discussionManager.activatedDiscussion) { + this.discussionManager.showDiscussionPart(); + } + } + + public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){ + this.discussionManager.onSendMessageCallback(userId, callback); + } + + get activatedDiscussion(){ + return this.discussionManager.activatedDiscussion; + } + + public setUserInputManager(userInputManager : UserInputManager){ + this.discussionManager.setUserInputManager(userInputManager); + } } export const mediaManager = new MediaManager(); diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts index 3efee1c3..a6cf679b 100644 --- a/front/src/WebRtc/ScreenSharingPeer.ts +++ b/front/src/WebRtc/ScreenSharingPeer.ts @@ -2,6 +2,7 @@ import * as SimplePeerNamespace from "simple-peer"; import {mediaManager} from "./MediaManager"; import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "../Connexion/RoomConnection"; +import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); @@ -148,6 +149,6 @@ export class ScreenSharingPeer extends Peer { public stopPushingScreenSharingToRemoteUser(stream: MediaStream) { this.removeStream(stream); - this.write(new Buffer(JSON.stringify({streamEnded: true}))); + this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true}))); } } diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index eb2ee42b..195b57b3 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -10,7 +10,7 @@ import { UpdatedLocalStreamCallback } from "./MediaManager"; import {ScreenSharingPeer} from "./ScreenSharingPeer"; -import {VideoPeer} from "./VideoPeer"; +import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import {RoomConnection} from "../Connexion/RoomConnection"; export interface UserSimplePeerInterface{ @@ -38,7 +38,7 @@ export class SimplePeer { private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback; private readonly peerConnectionListeners: Array = new Array(); - constructor(private Connection: RoomConnection, private enableReporting: boolean) { + constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) { // We need to go through this weird bound function pointer in order to be able to "free" this reference later. this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); @@ -145,6 +145,12 @@ export class SimplePeer { mediaManager.addActiveVideo("" + user.userId, reportCallback, name); const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); + + //permit to send message + mediaManager.addSendMessageCallback(user.userId,(message: string) => { + peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), message: message}))); + }); + peer.toClose = false; // When a connection is established to a video stream, and if a screen sharing is taking place, // the user sharing screen should also initiate a connection to the remote user! @@ -318,7 +324,7 @@ export class SimplePeer { throw new Error('While adding media, cannot find user with ID ' + userId); } const localStream: MediaStream | null = mediaManager.localStream; - PeerConnection.write(new Buffer(JSON.stringify(mediaManager.constraintsMedia))); + PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia}))); if(!localStream){ return; diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index fb34f29e..f8bfa3f9 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -5,6 +5,8 @@ import {RoomConnection} from "../Connexion/RoomConnection"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); +export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; +export const MESSAGE_TYPE_MESSAGE = 'message'; /** * A peer connection used to transmit video / audio signals between 2 peers. */ @@ -78,19 +80,27 @@ export class VideoPeer extends Peer { }); this.on('data', (chunk: Buffer) => { - const constraint = JSON.parse(chunk.toString('utf8')); - console.log("data", constraint); - if (constraint.audio) { - mediaManager.enabledMicrophoneByUserId(this.userId); - } else { - mediaManager.disabledMicrophoneByUserId(this.userId); + const message = JSON.parse(chunk.toString('utf8')); + console.log("data", message); + + if(message.type === MESSAGE_TYPE_CONSTRAINT) { + const constraint = message; + if (constraint.audio) { + mediaManager.enabledMicrophoneByUserId(this.userId); + } else { + mediaManager.disabledMicrophoneByUserId(this.userId); + } + + if (constraint.video || constraint.screen) { + mediaManager.enabledVideoByUserId(this.userId); + } else { + this.stream(undefined); + mediaManager.disabledVideoByUserId(this.userId); + } } - if (constraint.video || constraint.screen) { - mediaManager.enabledVideoByUserId(this.userId); - } else { - this.stream(undefined); - mediaManager.disabledVideoByUserId(this.userId); + if(message.type === 'message') { + mediaManager.addNewMessage(message.name, message.message); } }); @@ -163,7 +173,7 @@ export class VideoPeer extends Peer { private pushVideoToRemoteUser() { try { const localStream: MediaStream | null = mediaManager.localStream; - this.write(new Buffer(JSON.stringify(mediaManager.constraintsMedia))); + this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia}))); if(!localStream){ return; diff --git a/front/webpack.config.js b/front/webpack.config.js index 218b7374..f03c4fbc 100644 --- a/front/webpack.config.js +++ b/front/webpack.config.js @@ -45,7 +45,7 @@ module.exports = { new webpack.ProvidePlugin({ Phaser: 'phaser' }), - new webpack.EnvironmentPlugin(['API_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE']) + new webpack.EnvironmentPlugin(['API_URL', 'ADMIN_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE']) ], }; diff --git a/maps/characters/tenue_sg_1.png b/maps/characters/tenue_sg_1.png new file mode 100644 index 00000000..e334340e Binary files /dev/null and b/maps/characters/tenue_sg_1.png differ diff --git a/maps/characters/tenue_sg_2.png b/maps/characters/tenue_sg_2.png new file mode 100644 index 00000000..fe717a46 Binary files /dev/null and b/maps/characters/tenue_sg_2.png differ diff --git a/messages/messages.proto b/messages/messages.proto index 89353825..6e0b47df 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -38,6 +38,10 @@ message CharacterLayerMessage { /*********** CLIENT TO SERVER MESSAGES *************/ +message PingMessage { + +} + message SetPlayerDetailsMessage { string name = 1; repeated string characterLayers = 2; diff --git a/website/dist/index.html b/website/dist/index.html index 1e06204a..df76814d 100644 --- a/website/dist/index.html +++ b/website/dist/index.html @@ -10,6 +10,9 @@ gtag('js', new Date()); gtag('config', 'UA-10196481-11'); + if (window.location.host.endsWith("localhost")){ + window['ga-disable-UA-10196481-11'] = true; + } @@ -82,22 +85,32 @@
-

Your workplace
but better

-

You are impatient to discover this new world? Click on "Work online" and meet new people or share this adventure with your colleagues and friends by clicking on "Work in private"

+

Meet your teammates

+

+ WorkAdventure preserves your social interaction while COVID is still out there. +

+

+ Stay connected with your teamworkers, by creating your own online workspace to work remotely. +

+

+ Stay connected with your clients by providing a dedicated digital place to organize meetings, workshops. +

+

+ Stay connected with your future collaborators by organizing online event. +

@@ -107,7 +120,7 @@
@@ -158,7 +171,7 @@

Click the button below to come and say hi!

- START IN PUBLIC MODE + TRY IT NOW !

@@ -166,10 +179,11 @@

You can also create a private room with your friends or your team !

To try, press button work in private

-

- - START WORKING IN PRIVATE -

+

+ + + CHOOSE YOU OWN MAP +

Don’t forget to activate your mic and camera, let’s play

@@ -179,7 +193,6 @@