diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 9c46a41b..3e2dd3e0 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -100,11 +100,12 @@ class AdminApi { return res.data; } - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { + reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { return Axios.post(`${ADMIN_API_URL}/api/report`, { reportedUserUuid, reportedUserComment, reporterUserUuid, + reportWorldSlug, }, { headers: {"Authorization": `${ADMIN_API_TOKEN}`} diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html index 88c76ca2..665d4316 100644 --- a/front/dist/resources/html/gameMenu.html +++ b/front/dist/resources/html/gameMenu.html @@ -15,6 +15,14 @@ #gameMenu section { margin: 10px; } + section#socialLinks{ + position: absolute; + margin-bottom: 0; + } + section#socialLinks img{ + width: 32px; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; + } diff --git a/front/dist/resources/html/gameReport.html b/front/dist/resources/html/gameReport.html new file mode 100644 index 00000000..9a761c32 --- /dev/null +++ b/front/dist/resources/html/gameReport.html @@ -0,0 +1,104 @@ + + + diff --git a/front/dist/resources/logos/logo.png b/front/dist/resources/logos/logo.png new file mode 100644 index 00000000..f4440ad5 Binary files /dev/null and b/front/dist/resources/logos/logo.png differ diff --git a/front/dist/resources/logos/report.back.svg b/front/dist/resources/logos/report.back.svg new file mode 100644 index 00000000..1cb3b068 --- /dev/null +++ b/front/dist/resources/logos/report.back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/logos/report.svg b/front/dist/resources/logos/report.svg index 1cb3b068..14753256 100644 --- a/front/dist/resources/logos/report.svg +++ b/front/dist/resources/logos/report.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/front/dist/resources/objects/facebook-icon.png b/front/dist/resources/objects/facebook-icon.png new file mode 100644 index 00000000..7b74b9bf Binary files /dev/null and b/front/dist/resources/objects/facebook-icon.png differ diff --git a/front/dist/resources/objects/talk.png b/front/dist/resources/objects/talk.png index b9ecdb30..bc06d3b0 100644 Binary files a/front/dist/resources/objects/talk.png and b/front/dist/resources/objects/talk.png differ diff --git a/front/dist/resources/objects/twitter-icon.png b/front/dist/resources/objects/twitter-icon.png new file mode 100644 index 00000000..f2fa90f1 Binary files /dev/null and b/front/dist/resources/objects/twitter-icon.png differ diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 2e2c6c10..111700d1 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -39,6 +39,7 @@ body .message-info.warning{ position: relative; transition: all 0.2s ease; background-color: #00000099; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; } .video-container i{ position: absolute; @@ -53,25 +54,62 @@ body .message-info.warning{ font-size: 28px; color: white; } -.video-container img.active{ - display: block; -} + .video-container img{ position: absolute; display: none; - width: 15px; - height: 15px; - background: #d93025; - border-radius: 48px; + width: 25px; + height: 25px; left: 5px; bottom: 5px; padding: 10px; z-index: 2; } -.video-container img.report{ +.video-container button.report{ + display: block; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; + background: none; + background-color: rgba(0, 0, 0, 0); + border: none; + background-color: black; + border-radius: 15px; + position: absolute; + width: 0px; + height: 35px; right: 5px; - left: auto; + bottom: 5px; + padding: 0px; + overflow: hidden; + z-index: 2; + transition: all .5s ease; +} + +.video-container:hover button.report{ + width: 35px; + padding: 10px; +} + +.video-container button.report:hover { + width: 94px; +} + +.video-container button.report img{ + position: absolute; + display: block; + bottom: 5px; + left: 5px; + margin: 0; + padding: 0; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; +} +.video-container button.report span{ + position: absolute; + bottom: 8px; + left: 36px; + color: white; + font-size: 16px; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; } .video-container video{ @@ -352,6 +390,7 @@ body { #cowebsite { position: fixed; transition: transform 0.5s; + background-color: white; } #cowebsite.loading { background-color: gray; @@ -1078,7 +1117,7 @@ div.modal-report-user{ white-space: pre-wrap; word-wrap: break-word; } -.discussion .messages .message p.a{ +.discussion .messages .message p a{ color: white; } diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index ab9c0d95..0eda58dc 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -1,14 +1,50 @@ +import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig; -export const addLoader = (scene:Phaser.Scene): void => { - const loadingText = scene.add.text(scene.game.renderer.width / 2, 200, 'Loading'); +const LogoNameIndex: string = 'logoLoading'; +const TextName: string = 'Loading...'; +const LogoResource: string = 'resources/logos/logo.png'; +const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59}; + +export const addLoader = (scene: Phaser.Scene): void => { + let loadingText: Phaser.GameObjects.Text|null = null; + const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3); + const loadingBarHeight: number = 16; + const padding: number = 5; + + const promiseLoadLogoTexture = new Promise((res) => { + if(scene.load.textureManager.exists(LogoNameIndex)){ + return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)); + }else{ + //add loading if logo image is not ready + loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName); + } + scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame); + scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { + if(loadingText){ + loadingText.destroy(); + } + return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex)); + }); + }); + + const progressContainer = scene.add.graphics(); const progress = scene.add.graphics(); + progressContainer.fillStyle(0x444444, 0.8); + progressContainer.fillRect((scene.game.renderer.width - loadingBarWidth) / 2 - padding, scene.game.renderer.height / 2 + 50 - padding, loadingBarWidth + padding * 2, loadingBarHeight + padding * 2); + scene.load.on('progress', (value: number) => { progress.clear(); - progress.fillStyle(0xffffff, 1); - progress.fillRect(0, 270, 800 * value, 60); + progress.fillStyle(0xBBBBBB, 1); + progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight); }); scene.load.on('complete', () => { - loadingText.destroy(); + if(loadingText){ + loadingText.destroy(); + } + promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => { + resLoadingImage.destroy(); + }); progress.destroy(); + progressContainer.destroy(); }); -} \ No newline at end of file +} diff --git a/front/src/Phaser/Components/OpenChatIcon.ts b/front/src/Phaser/Components/OpenChatIcon.ts index bf293bab..1e9429e8 100644 --- a/front/src/Phaser/Components/OpenChatIcon.ts +++ b/front/src/Phaser/Components/OpenChatIcon.ts @@ -3,14 +3,12 @@ import {discussionManager} from "../../WebRtc/DiscussionManager"; export const openChatIconName = 'openChatIcon'; export class OpenChatIcon extends Phaser.GameObjects.Image { constructor(scene: Phaser.Scene, x: number, y: number) { - super(scene, x, y, openChatIconName); + super(scene, x, y, openChatIconName, 3); scene.add.existing(this); this.setScrollFactor(0, 0); this.setOrigin(0, 1); - this.displayWidth = 30; - this.displayHeight = 30; this.setInteractive(); - this.setVisible(false) + this.setVisible(false); this.setDepth(99999); this.on("pointerup", () => discussionManager.showDiscussionPart()); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4f959ee6..5ed2db87 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -154,7 +154,7 @@ export class GameScene extends ResizableScene implements CenterListener { private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. private outlinedItem: ActionableItem|null = null; - private userInputManager!: UserInputManager; + public userInputManager!: UserInputManager; private isReconnecting: boolean = false; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; @@ -644,7 +644,7 @@ export class GameScene extends ResizableScene implements CenterListener { coWebsiteManager.closeCoWebsite(); }else{ const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string); + coWebsiteManager.loadCoWebsite(newValue as string, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; @@ -1202,6 +1202,7 @@ export class GameScene extends ResizableScene implements CenterListener { private reposition(): void { this.presentationModeSprite.setY(this.game.renderer.height - 2); this.chatModeSprite.setY(this.game.renderer.height - 2); + this.openChatIcon.setY(this.game.renderer.height - 2); // Recompute camera offset if needed this.updateCameraOffset(); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 8bd64cd1..872af30c 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -2,14 +2,17 @@ import {LoginScene, LoginSceneName} from "../Login/LoginScene"; import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {gameManager} from "../Game/GameManager"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; +import {mediaManager, ReportCallback, ShowReportCallBack} from "../../WebRtc/MediaManager"; import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; +import {GameConnexionTypes} from "../../Url/UrlManager"; +import {connectionManager} from "../../Connexion/ConnectionManager"; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; const gameMenuIconKey = 'gameMenuIcon'; const gameSettingsMenuKey = 'gameSettingsMenu'; const gameShare = 'gameShare'; +const gameReport = 'gameReport'; const closedSideMenuX = -200; const openedSideMenuX = 0; @@ -21,9 +24,11 @@ export class MenuScene extends Phaser.Scene { private menuElement!: Phaser.GameObjects.DOMElement; private gameQualityMenuElement!: Phaser.GameObjects.DOMElement; private gameShareElement!: Phaser.GameObjects.DOMElement; + private gameReportElement!: Phaser.GameObjects.DOMElement; private sideMenuOpened = false; private settingsMenuOpened = false; private gameShareOpened = false; + private gameReportOpened = false; private gameQualityValue: number; private videoQualityValue: number; private menuButton!: Phaser.GameObjects.DOMElement; @@ -40,6 +45,7 @@ export class MenuScene extends Phaser.Scene { this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameShare, 'resources/html/gameShare.html'); + this.load.html(gameReport, 'resources/html/gameReport.html'); } create() { @@ -64,6 +70,19 @@ export class MenuScene extends Phaser.Scene { } }); + this.gameReportElement = this.add.dom(middleX, -400).createFromCache(gameReport); + this.revealMenusAfterInit(this.gameReportElement, gameReport); + this.gameReportElement.addListener('click'); + this.gameReportElement.on('click', (event:MouseEvent) => { + event.preventDefault(); + if((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') { + this.submitReport(); + }else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') { + this.closeGameReport(); + } + }); + mediaManager.setShowReportModalCallBacks(this.openGameReport.bind(this)); + this.input.keyboard.on('keyup-TAB', () => { this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); }); @@ -98,6 +117,11 @@ export class MenuScene extends Phaser.Scene { const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement; adminSection.hidden = false; } + //TODO bind with future metadata of card + //if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){ + const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; + adminSection.hidden = false; + //} this.tweens.add({ targets: this.menuElement, x: openedSideMenuX, @@ -221,7 +245,75 @@ export class MenuScene extends Phaser.Scene { }); } + private openGameReport(userId: string, userName: string|undefined){ + if (this.gameReportOpened) { + this.closeGameReport(); + return; + } + + //close all + this.closeAll(); + + const gameTitleReport = this.gameReportElement.getChildByID('nameReported') as HTMLElement; + gameTitleReport.innerText = userName ? `Report user: ${userName}` : 'Report user'; + const gameIdUserReported = this.gameReportElement.getChildByID('idUserReported') as HTMLInputElement; + gameIdUserReported.value = userId; + + this.gameReportOpened = true; + let middleY = (window.innerHeight / 3) - (257); + if(middleY < 0){ + middleY = 0; + } + let middleX = (window.innerWidth / 3) - 298; + if(middleX < 0){ + middleX = 0; + } + + gameManager.getCurrentGameScene(this).userInputManager.clearAllInputKeyboard(); + + this.tweens.add({ + targets: this.gameReportElement, + y: middleY, + x: middleX, + duration: 1000, + ease: 'Power3' + }); + return; + } + + private closeGameReport(): void{ + this.gameReportOpened = false; + gameManager.getCurrentGameScene(this).userInputManager.initKeyBoardEvent(); + this.tweens.add({ + targets: this.gameReportElement, + y: -400, + duration: 1000, + ease: 'Power3' + }); + } + + private submitReport(): void{ + const gamePError = this.gameReportElement.getChildByID('gameReportErr') as HTMLParagraphElement; + gamePError.innerText = ''; + gamePError.style.display = 'none'; + const gameTextArea = this.gameReportElement.getChildByID('gameReportInput') as HTMLInputElement; + const gameIdUserReported = this.gameReportElement.getChildByID('idUserReported') as HTMLInputElement; + if(!gameTextArea || !gameTextArea.value || !gameIdUserReported || !gameIdUserReported.value){ + gamePError.innerText = 'Report message cannot to be empty.'; + gamePError.style.display = 'block'; + return; + } + gameManager.getCurrentGameScene(this).connection.emitReportPlayerMessage( + parseInt(gameIdUserReported.value), + gameTextArea.value + ); + this.closeGameReport(); + } + private onMenuClick(event:MouseEvent) { + if((event?.target as HTMLInputElement).classList.contains('not-button')){ + return; + } event.preventDefault(); switch ((event?.target as HTMLInputElement).id) { @@ -280,5 +372,6 @@ export class MenuScene extends Phaser.Scene { private closeAll(){ this.closeGameQualityMenu(); this.closeGameShare(); + this.closeGameReport(); } } diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index e9564222..ef73ac1d 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -42,7 +42,7 @@ class CoWebsiteManager { this.opened = iframeStates.opened; } - public loadCoWebsite(url: string): void { + public loadCoWebsite(url: string, allowPolicy?: string): void { this.load(); this.cowebsiteDiv.innerHTML = ` + `:'' + ) + ` @@ -490,18 +500,23 @@ export class MediaManager { layoutManager.add(DivImportance.Normal, userId, html); - if (reportCallBack) { - const reportBtn = HtmlUtils.getElementByIdOrFail(`report-${userId}`); - reportBtn.addEventListener('click', (e: MouseEvent) => { - e.preventDefault(); - this.showReportModal(userId, userName, reportCallBack); - }); - } - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); //permit to create participant in discussion part - this.addNewParticipant(userId, userName, undefined, reportCallBack); + const showReportUser = () => { + for(const callBack of this.showReportModalCallBacks){ + callBack(userId, userName); + } + }; + this.addNewParticipant(userId, userName, undefined, showReportUser); + + if(!anonymous){ + const reportBanUserAction: HTMLImageElement = HtmlUtils.getElementByIdOrFail(`report-${userId}`); + reportBanUserAction.addEventListener('click', (e) => { + e.preventDefault(); + showReportUser(); + }); + } } addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ @@ -645,65 +660,8 @@ export class MediaManager { return color; } - public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){ - //create report text area - const mainContainer = HtmlUtils.getElementByIdOrFail('main-container'); - - const divReport = document.createElement('div'); - divReport.classList.add('modal-report-user'); - - const inputHidden = document.createElement('input'); - inputHidden.id = 'input-report-user'; - inputHidden.type = 'hidden'; - inputHidden.value = userId; - divReport.appendChild(inputHidden); - - const titleMessage = document.createElement('p'); - titleMessage.id = 'title-report-user'; - titleMessage.innerText = 'Open a report'; - divReport.appendChild(titleMessage); - - const bodyMessage = document.createElement('p'); - bodyMessage.id = 'body-report-user'; - bodyMessage.innerText = `You are about to open a report regarding an offensive conduct from user ${userName.toUpperCase()}. Please explain to us how you think ${userName.toUpperCase()} breached the code of conduct.`; - divReport.appendChild(bodyMessage); - - const imgReportUser = document.createElement('img'); - imgReportUser.id = 'img-report-user'; - imgReportUser.src = 'resources/logos/report.svg'; - divReport.appendChild(imgReportUser); - - const textareaUser = document.createElement('textarea'); - textareaUser.id = 'textarea-report-user'; - textareaUser.placeholder = 'Write ...'; - divReport.appendChild(textareaUser); - - const buttonReport = document.createElement('button'); - buttonReport.id = 'button-save-report-user'; - buttonReport.innerText = 'Report'; - buttonReport.addEventListener('click', () => { - if(!textareaUser.value){ - textareaUser.style.border = '1px solid red' - return; - } - reportCallBack(textareaUser.value); - divReport.remove(); - }); - divReport.appendChild(buttonReport); - - const buttonCancel = document.createElement('img'); - buttonCancel.id = 'cancel-report-user'; - buttonCancel.src = 'resources/logos/close.svg'; - buttonCancel.addEventListener('click', () => { - divReport.remove(); - }); - divReport.appendChild(buttonCancel); - - mainContainer.appendChild(divReport); - } - - public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){ - discussionManager.addParticipant(userId, name, img, false, reportCallBack); + public addNewParticipant(userId: number|string, name: string|undefined, img?: string, showReportUserCallBack?: ShowReportCallBack){ + discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack); } public removeParticipant(userId: number|string){ @@ -769,6 +727,10 @@ export class MediaManager { this.checkActiveUser(); }, this.focused ? 10000 : 1000); } + + public setShowReportModalCallBacks(callback: ShowReportCallBack){ + this.showReportModalCallBacks.add(callback); + } } export const mediaManager = new MediaManager(); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 90d260ee..bc2590d7 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -11,6 +11,8 @@ import { import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import {RoomConnection} from "../Connexion/RoomConnection"; +import {connectionManager} from "../Connexion/ConnectionManager"; +import {GameConnexionTypes} from "../Url/UrlManager"; export interface UserSimplePeerInterface{ userId: number; @@ -134,11 +136,7 @@ export class SimplePeer { mediaManager.removeActiveVideo("" + user.userId); - const reportCallback = this.enableReporting ? (comment: string) => { - this.reportUser(user.userId, comment); - } : undefined; - - mediaManager.addActiveVideo("" + user.userId, reportCallback, name); + mediaManager.addActiveVideo("" + user.userId, name, connectionManager.getConnexionType === GameConnexionTypes.anonymous); const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); @@ -391,13 +389,6 @@ export class SimplePeer { } } - /** - * Triggered locally when clicking on the report button - */ - public reportUser(userId: number, message: string) { - this.Connection.emitReportPlayerMessage(userId, message) - } - private sendLocalScreenSharingStreamToUser(userId: number): void { // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) if (this.PeerScreenSharingConnectionArray.has(userId)) { diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index 48e8a1a4..e9bccef8 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -105,11 +105,12 @@ class AdminApi { return res.data; } - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { + reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { return Axios.post(`${ADMIN_API_URL}/api/report`, { reportedUserUuid, reportedUserComment, reporterUserUuid, + reportWorldSlug }, { headers: {"Authorization": `${ADMIN_API_TOKEN}`} diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 2f86ae19..7bd50b32 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -304,7 +304,7 @@ export class SocketManager implements ZoneEventListener { throw 'reported socket user not found'; } //TODO report user on admin application - await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) + await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2]) } catch (e) { console.error('An error occurred on "handleReportMessage"'); console.error(e);