diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index e33346d0..9c91e0ca 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -190,6 +190,8 @@ jobs: SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }} TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }} DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} + POSTHOG_URL: ${{ secrets.POSTHOG_URL }} with: namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 710b85fd..d4e7db82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,41 @@ ## Version develop +## Version 1.5.0 +### Updates +- Added support for login with OpenID Connect +- New scripting library available to extend WorkAdventure: see [Scripting API Extra](https://github.com/workadventure/scripting-api-extra/) +- New menu design! +- New `openTab` property (#1419) +- Possible integration with Posthog (#1458) + +### Bugfix +- Fixing layers flattened several times (#1427 @Lurkars) +- Fixing CSS of video elements +- Chat now scrolls to bottom when opened (#1450) +- Fixing silent zone not respected when exiting from Jitsi (#1456) +- Fixing "yarn install" failing because of missing rights on some Docker installs (#1457) +- Fixing audio not shut down when exiting a room (#1459) + +### Misc +- Finished migrating "Build your map" documentation into the "/docs" directory of this repository (#1417 #1385) +- Refactoring documentation (dedicated page for variables) (#1414) +- Front container code is now completely linted (#1413) + +## Version 1.4.15 + ### Updates - New scripting API features : - Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu. +- New `jitsiWidth` parameter to set the width of Jitsi and Cowebsite (#1398 @tabascoeye) +- Refactored the way videos are displayed to better cope for vertical videos (on mobile) +- Fixing reconnection issues after 5 minutes of an inactive tab on Google Chrome +- Changes performed in `WA.room.setPropertyLayer` now have a real-time impact (#1395) + +### Bugfixes +- Fixing streams in bubbles sometimes improperly muted when there are more than 2 people in the bubble (#1400 #1402) +- Properly displaying carriage returns in popups (#1388) +- `WA.state` now answers correctly to "in" keyword (#1393) +- Variables can now be nested in group layers (#1406) ## Version 1.4.14 diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 92f62b0b..493f97a2 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -9,7 +9,6 @@ const JITSI_ISS = process.env.JITSI_ISS || ""; const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080; const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051; -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 const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ""; export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export const REDIS_HOST = process.env.REDIS_HOST || undefined; diff --git a/deeployer.libsonnet b/deeployer.libsonnet index a2e8970a..75a4de8c 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -75,6 +75,9 @@ "UPLOADER_URL": "//uploader-"+url, "ADMIN_URL": "//"+url, "JITSI_URL": env.JITSI_URL, + #POSTHOG + "POSTHOG_API_KEY": if namespace == "master" then env.POSTHOG_API_KEY else "", + "POSTHOG_URL": if namespace == "master" then env.POSTHOG_URL else "", "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", diff --git a/docs/maps/opening-a-website.md b/docs/maps/opening-a-website.md index 682306b4..2ca54281 100644 --- a/docs/maps/opening-a-website.md +++ b/docs/maps/opening-a-website.md @@ -12,7 +12,7 @@ In order to create a zone that opens websites: * You must create a specific layer. * In layer properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://") -* You may also use "`openWebsiteWidth`" property (of type "`number`" between 0 and 100) to control the width of the iframe. +* You may also use "`openWebsiteWidth`" property (of type "`int`" or "`float`" between 0 and 100) to control the width of the iframe. * You may also use "`openTab`" property (of type "`string`") to open in a new tab instead. {.alert.alert-warning} diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md index bdaf8a42..36875719 100644 --- a/docs/maps/wa-maps.md +++ b/docs/maps/wa-maps.md @@ -56,6 +56,8 @@ A few things to notice: ## Building walls and "collidable" areas +[Building your map - Collides](https://www.youtube.com/watch?v=qTK50ymhMIE) + By default, the characters can traverse any tiles. If you want to prevent your character from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile. To make a tile "collidable", you should: diff --git a/front/Dockerfile b/front/Dockerfile index ef724e0f..6fef9dc8 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,13 +1,13 @@ -FROM thecodingmachine/workadventure-back-base:latest as builder -WORKDIR /var/www/messages -COPY --chown=docker:docker messages . +FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder +WORKDIR /usr/src +COPY messages . RUN yarn install && yarn proto # we are rebuilding on each deploy to cope with the PUSHER_URL environment URL FROM thecodingmachine/nodejs:14-apache COPY --chown=docker:docker front . -COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated +COPY --from=builder --chown=docker:docker /usr/src/generated /var/www/html/src/Messages/generated # Removing the iframe.html file from the final image as this adds a XSS attack. # iframe.html is only in dev mode to circumvent a limitation diff --git a/front/package.json b/front/package.json index 31ddc651..44d3fb22 100644 --- a/front/package.json +++ b/front/package.json @@ -50,6 +50,7 @@ "phaser": "^3.54.0", "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser3-rex-plugins": "^1.1.42", + "posthog-js": "^1.13.12", "queue-typescript": "^1.0.1", "quill": "1.3.6", "quill-delta-to-html": "^0.12.0", diff --git a/front/src/Administration/AnalyticsClient.ts b/front/src/Administration/AnalyticsClient.ts new file mode 100644 index 00000000..f73a1981 --- /dev/null +++ b/front/src/Administration/AnalyticsClient.ts @@ -0,0 +1,74 @@ +import { POSTHOG_API_KEY, POSTHOG_URL } from "../Enum/EnvironmentVariable"; + +class AnalyticsClient { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private posthogPromise: Promise; + + constructor() { + if (POSTHOG_API_KEY && POSTHOG_URL) { + this.posthogPromise = import("posthog-js").then(({ default: posthog }) => { + posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_URL, disable_cookie: true }); + return posthog; + }); + } else { + this.posthogPromise = Promise.reject(); + } + } + + identifyUser(uuid: string) { + this.posthogPromise + .then((posthog) => { + posthog.identify(uuid, { uuid, wa: true }); + }) + .catch(); + } + + loggedWithSso() { + this.posthogPromise + .then((posthog) => { + posthog.capture("wa-logged-sso"); + }) + .catch(); + } + + loggedWithToken() { + this.posthogPromise + .then((posthog) => { + posthog.capture("wa-logged-token"); + }) + .catch(); + } + + enteredRoom(roomId: string) { + this.posthogPromise + .then((posthog) => { + posthog.capture("$pageView", { roomId }); + }) + .catch(); + } + + openedMenu() { + this.posthogPromise + .then((posthog) => { + posthog.capture("wa-opened-menu"); + }) + .catch(); + } + + launchEmote(emote: string) { + this.posthogPromise + .then((posthog) => { + posthog.capture("wa-emote-launch", { emote }); + }) + .catch(); + } + + enteredJitsi(roomName: string, roomId: string) { + this.posthogPromise + .then((posthog) => { + posthog.capture("wa-entered-jitsi", { roomName, roomId }); + }) + .catch(); + } +} +export const analyticsClient = new AnalyticsClient(); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 140d2f34..5a9aca85 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -46,30 +46,18 @@ type AnswererCallback = ( * Also allows to send messages to those iframes. */ class IframeListener { - private readonly _readyStream: Subject = new Subject(); - public readonly readyStream = this._readyStream.asObservable(); - - private readonly _chatStream: Subject = new Subject(); - public readonly chatStream = this._chatStream.asObservable(); - private readonly _openPopupStream: Subject = new Subject(); public readonly openPopupStream = this._openPopupStream.asObservable(); private readonly _openTabStream: Subject = new Subject(); public readonly openTabStream = this._openTabStream.asObservable(); - private readonly _goToPageStream: Subject = new Subject(); - public readonly goToPageStream = this._goToPageStream.asObservable(); - private readonly _loadPageStream: Subject = new Subject(); public readonly loadPageStream = this._loadPageStream.asObservable(); private readonly _openCoWebSiteStream: Subject = new Subject(); public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable(); - private readonly _closeCoWebSiteStream: Subject = new Subject(); - public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable(); - private readonly _disablePlayerControlStream: Subject = new Subject(); public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable(); @@ -219,7 +207,7 @@ class IframeListener { } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { this._setPropertyStream.next(payload.data); } else if (payload.type === "chat" && isChatEvent(payload.data)) { - this._chatStream.next(payload.data); + scriptUtils.sendAnonymousChat(payload.data); } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { this._openPopupStream.next(payload.data); } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts index 0dbe40fe..ad6dcc0f 100644 --- a/front/src/Api/ScriptUtils.ts +++ b/front/src/Api/ScriptUtils.ts @@ -1,4 +1,7 @@ import { coWebsiteManager } from "../WebRtc/CoWebsiteManager"; +import { playersStore } from "../Stores/PlayersStore"; +import { chatMessagesStore } from "../Stores/ChatStore"; +import type { ChatEvent } from "./Events/ChatEvent"; class ScriptUtils { public openTab(url: string) { @@ -16,6 +19,11 @@ class ScriptUtils { public closeCoWebSite() { coWebsiteManager.closeCoWebsite(); } + + public sendAnonymousChat(chatEvent: ChatEvent) { + const userId = playersStore.addFacticePlayer(chatEvent.author); + chatMessagesStore.addExternalMessage(userId, chatEvent.message); + } } export const scriptUtils = new ScriptUtils(); diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index a448d56c..a7f96f61 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -5,9 +5,9 @@ audioManagerFileStore, audioManagerVolumeStore, } from "../../Stores/AudioManagerStore"; - import {get} from "svelte/store"; + import { get } from "svelte/store"; import type { Unsubscriber } from "svelte/store"; - import {onDestroy, onMount} from "svelte"; + import { onDestroy, onMount } from "svelte"; let HTMLAudioPlayer: HTMLAudioElement; let audioPlayerVolumeIcon: HTMLElement; @@ -21,9 +21,9 @@ onMount(() => { volume = localUserStore.getAudioPlayerVolume(); audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted()); - setVolume(); + changeVolume(); - unsubscriberFileStore = audioManagerFileStore.subscribe(() =>{ + unsubscriberFileStore = audioManagerFileStore.subscribe(() => { HTMLAudioPlayer.pause(); HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop; HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume; @@ -53,13 +53,7 @@ } }) - function onMute() { - audioManagerVolumeStore.setMuted(!get(audioManagerVolumeStore).muted); - localUserStore.setAudioPlayerMuted(get(audioManagerVolumeStore).muted); - setVolume(); - } - - function setVolume() { + function changeVolume() { if (get(audioManagerVolumeStore).muted) { audioPlayerVolumeIcon.classList.add('muted'); audioPlayerVol.value = "0"; @@ -76,8 +70,22 @@ audioPlayerVolumeIcon.classList.remove('mid'); } } - audioManagerVolumeStore.setVolume(volume) - localUserStore.setAudioPlayerVolume(get(audioManagerVolumeStore).volume); + } + + function onMute() { + const muted = !get(audioManagerVolumeStore).muted; + audioManagerVolumeStore.setMuted(muted); + localUserStore.setAudioPlayerMuted(muted); + changeVolume(); + } + + function setVolume() { + volume = parseFloat(audioPlayerVol.value); + audioManagerVolumeStore.setVolume(volume); + localUserStore.setAudioPlayerVolume(volume); + audioManagerVolumeStore.setMuted(false); + localUserStore.setAudioPlayerMuted(false); + changeVolume(); } function setDecrease() { @@ -108,8 +116,7 @@ - +