diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index d1f5e7f7..dec0630f 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -689,16 +689,17 @@ div.modal-report-user{ /*MESSAGE*/ .discussion{ position: fixed; - right: -300px; + left: -300px; width: 220px; height: 100%; - background-color: #000000; + background-color: #333333; padding: 20px; + transition: all 0.5s ease; } .discussion.active{ - right: 0; + left: 0; } -.discussion .toggle-btn{ +.discussion .active-btn{ display: none; cursor: pointer; height: 50px; @@ -706,22 +707,36 @@ div.modal-report-user{ background-color: #2d2d2dba; position: absolute; top: calc(50% - 25px); - margin-left: -125px; + margin-left: 315px; border-radius: 50%; border: none; transition: all 0.5s ease; } -.discussion .toggle-btn.active{ +.discussion .active-btn.active{ display: block; } -.discussion .toggle-btn:hover { +.discussion .active-btn:hover { transform: scale(1.1) rotateY(3.142rad); } -.discussion .toggle-btn img{ +.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; @@ -793,13 +808,22 @@ div.modal-report-user{ .discussion .messages{ position: absolute; - height: calc(100% - 300px); + height: calc(100% - 360px); + overflow-x: hidden; + overflow-y: scroll; + 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{ @@ -811,3 +835,38 @@ div.modal-report-user{ 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; +} + +.discussion .send-message img{ + position: absolute; + margin-right: 10px; + width: 20px; + height: 20px; + background-color: #ffffff69; +} +.discussion .send-message img:hover{ + background-color: #ffffff; +} + diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 55321124..2c5c1882 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -9,7 +9,6 @@ import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {SelectCharacterSceneName} from "./SelectCharacterScene"; import {ResizableScene} from "./ResizableScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {Discussion} from "../../WebRtc/Discussion"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -75,13 +74,6 @@ export class LoginScene extends ResizableScene { }); cypressAsserter.initFinished(); - - const discussion = new Discussion('test'); - discussion.addParticipant('GRP'); - discussion.addParticipant('LOL'); - - discussion.addMessage('GRP', 'ceci est un test d\'envoi de message', true); - discussion.addMessage('LOL', 'ceci est un test d\'envoi de message'); } update(time: number, delta: number): void { diff --git a/front/src/WebRtc/Discussion.ts b/front/src/WebRtc/Discussion.ts deleted file mode 100644 index b7307745..00000000 --- a/front/src/WebRtc/Discussion.ts +++ /dev/null @@ -1,124 +0,0 @@ -import {HtmlUtils} from "./HtmlUtils"; - -export class Discussion { - - private mainContainer: HTMLDivElement; - - private divDiscuss?: HTMLDivElement; - private divParticipants?: HTMLDivElement; - private nbpParticipants?: HTMLParagraphElement; - private divMessages?: HTMLParagraphElement; - - private participants: Map = new Map(); - - private activeDiscussion: boolean = false; - - constructor(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 buttonToggleDiscussion = document.createElement('button'); - buttonToggleDiscussion.classList.add('toggle-btn'); - buttonToggleDiscussion.classList.add('active'); - buttonToggleDiscussion.innerHTML = ``; - buttonToggleDiscussion.addEventListener('click', () => { - if(this.activeDiscussion){ - this.activeDiscussion = false; - this.divDiscuss?.classList.remove('active'); - buttonToggleDiscussion.classList.add('active'); - }else{ - this.activeDiscussion = true; - this.divDiscuss?.classList.add('active'); - buttonToggleDiscussion.classList.remove('active'); - } - }); - this.divDiscuss.appendChild(buttonToggleDiscussion); - - const myName = 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.divDiscuss.appendChild(myName); - this.divDiscuss.appendChild(this.nbpParticipants); - this.divDiscuss.appendChild(this.divParticipants); - this.divDiscuss.appendChild(this.divMessages); - - //append in main container - this.mainContainer.appendChild(this.divDiscuss); - } - - public addParticipant(name: string, img?: string) { - const divParticipant = document.createElement('div'); - divParticipant.classList.add('participant'); - - const divImgParticipant = document.createElement('img'); - divImgParticipant.src = 'resources/logos/boy.svg'; - if (img) { - divImgParticipant.src = img; - } - const divPParticipant = document.createElement('p'); - divPParticipant.innerText = name; - - const reportBanUserAction = document.createElement('button'); - reportBanUserAction.classList.add('report-btn') - reportBanUserAction.innerText = 'Report'; - reportBanUserAction.addEventListener('click', () => { - //TODO report user - console.log('report'); - }); - - divParticipant.appendChild(divImgParticipant); - divParticipant.appendChild(divPParticipant); - divParticipant.appendChild(reportBanUserAction); - - this.divParticipants?.appendChild(divParticipant); - this.participants.set(name, divParticipant); - - 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 = document.createElement('div'); - divMessage.classList.add('message'); - if(isMe){ - divMessage.classList.add('me'); - } - - const pMessage = document.createElement('p'); - const date = new Date(); - if(isMe){ - name = 'Moi'; - } - pMessage.innerHTML = `${name} - - ${date.getHours()}:${date.getMinutes()} - `; - divMessage.appendChild(pMessage); - - const userMessage = document.createElement('p'); - userMessage.innerText = message; - divMessage.appendChild(userMessage); - - this.divMessages?.appendChild(divMessage); - } - -} \ No newline at end of file diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts new file mode 100644 index 00000000..4663adcf --- /dev/null +++ b/front/src/WebRtc/DiscussionManager.ts @@ -0,0 +1,169 @@ +import {HtmlUtils} from "./HtmlUtils"; +import {UpdatedLocalStreamCallback} from "./MediaManager"; +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 participants: Map = new Map(); + + private activeDiscussion: boolean = false; + + private sendMessageCallBack : Set = new Set(); + + constructor(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'); + const buttonActiveDiscussion: HTMLButtonElement = document.createElement('button'); + buttonCloseDiscussion.classList.add('close-btn'); + buttonCloseDiscussion.innerHTML = ``; + buttonCloseDiscussion.addEventListener('click', () => { + this.activeDiscussion = false; + this.divDiscuss?.classList.remove('active'); + buttonActiveDiscussion.classList.add('active'); + }); + buttonActiveDiscussion.classList.add('active-btn'); + buttonActiveDiscussion.classList.add('active'); + buttonActiveDiscussion.innerHTML = ``; + buttonActiveDiscussion.addEventListener('click', () => { + this.activeDiscussion = true; + this.divDiscuss?.classList.add('active'); + buttonActiveDiscussion.classList.remove('active'); + }); + this.divDiscuss.appendChild(buttonCloseDiscussion); + this.divDiscuss.appendChild(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(); + this.addMessage(name, inputMessage.value, true); + for(const callback of this.sendMessageCallBack) { + 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) { + 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', () => { + //TODO report user + console.log('report'); + }); + divParticipant.appendChild(reportBanUserAction); + } + + this.divParticipants?.appendChild(divParticipant); + this.participants.set(userId, divParticipant); + + 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(); + } + } + + public onSendMessageCallback(callback: SendMessageCallback): void { + this.sendMessageCallBack.add(callback); + } + +} \ No newline at end of file diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 9c3563ad..660aa599 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,5 +1,6 @@ import {DivImportance, layoutManager} from "./LayoutManager"; import {HtmlUtils} from "./HtmlUtils"; +import {DiscussionManager, SendMessageCallback} from "./DiscussionManager"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any const videoConstraint: boolean|MediaTrackConstraints = { @@ -38,6 +39,8 @@ export class MediaManager { private cinemaBtn: HTMLDivElement; private monitorBtn: HTMLDivElement; + private discussionManager: DiscussionManager; + constructor() { this.myCamVideo = HtmlUtils.getElementByIdOrFail('myCamVideo'); @@ -89,6 +92,8 @@ export class MediaManager { this.disableScreenSharing(); //update tracking }); + + this.discussionManager = new DiscussionManager('test'); } public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { @@ -363,6 +368,9 @@ export class MediaManager { } this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); + + //permit to create participant in discussion part + this.addNewParticipant(userId, userName); } addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ @@ -437,6 +445,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}`) @@ -556,7 +567,21 @@ export class MediaManager { mainContainer.appendChild(divReport); } + public addNewParticipant(userId: number|string, name: string|undefined, img?: string){ + this.discussionManager.addParticipant(userId, name, img); + } + public removeParticipant(userId: number|string){ + this.discussionManager.removeParticipant(userId) + } + + public addNewMessage(name: string, message: string, isMe: boolean = false){ + this.discussionManager.addMessage(name, message, isMe) + } + + public addSendMessageCallback(callback: SendMessageCallback){ + this.discussionManager.onSendMessageCallback(callback) + } } 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..a9c24f17 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{ @@ -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((message: string) => { + peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: name?.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;