merged develop

This commit is contained in:
Hanusiak Piotr 2022-01-31 10:00:39 +01:00
commit 5aba99403e
144 changed files with 4800 additions and 2045 deletions

View file

@ -15,6 +15,9 @@ export class DebugController {
(async () => {
const query = parse(req.getQuery());
if (ADMIN_API_TOKEN === "") {
return res.writeStatus("401 Unauthorized").end("No token configured!");
}
if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
}

View file

@ -2,7 +2,7 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIM
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "";
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 || "";

View file

@ -1504,9 +1504,9 @@ natural-compare@^1.4.0:
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-fetch@^2.6.5:
version "2.6.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"

View file

@ -83,7 +83,8 @@
"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",
"START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json"
"START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json",
"ICON_URL": "//icon-"+url,
}
},
"uploader": {
@ -109,7 +110,15 @@
"redis": {
"image": "redis:6",
"ports": [6379]
}
},
"iconserver": {
"image": "matthiasluedtke/iconserver:v3.13.0",
"host": {
"url": "icon-"+url,
"containerPort": 8080,
},
"ports": [8080]
},
},
"config": {
k8sextension(k8sConf)::
@ -210,6 +219,16 @@
}
}
},
iconserver+: {
ingress+: {
spec+: {
tls+: [{
hosts: ["icon-"+url],
secretName: "certificate-tls"
}]
}
}
},
}
}
}

View file

@ -52,17 +52,17 @@ WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
### Opening/closing web page in Co-Websites
```
WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number = 0): Promise<CoWebsite>
WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number, closable: boolean, lazy: boolean): Promise<CoWebsite>
```
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open.
You can have only 5 co-wbesites open simultaneously.
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy
it's to add the cowebsite but don't load it.
Example:
```javascript
const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/');
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1);
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1, true, true);
// ...
coWebsite.close();
```

View file

@ -65,3 +65,24 @@ How to use entry point :
* To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`".
* You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
## Defining destination point with moveTo parameter
We are able to direct a Woka to the desired place immediately after spawn. To make users spawn on an entry point and then, walk automatically to a meeting room, simply add `moveTo` as an additional parameter of URL:
*Use default entry point*
```
.../my_map.json#&moveTo=exit
```
*Define entry point and moveTo parameter like this...*
```
.../my_map.json#start&moveTo=meeting-room
```
*...or like this*
```
.../my_map.json#moveTo=meeting-room&start
```
For this to work, moveTo must be equal to the layer name of interest. This layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected.
![](images/moveTo-layer-example.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,23 +1,35 @@
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
WORKDIR /usr/src
WORKDIR /usr/src/messages
COPY messages .
RUN yarn install && yarn ts-proto
# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL
FROM thecodingmachine/nodejs:14-apache
WORKDIR /usr/src/front
COPY front .
COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated
RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts
COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages
# move messages to front
RUN cp -r ../messages/ts-proto-generated/protos/* src/Messages/ts-proto-generated
RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' src/Messages/ts-proto-generated/messages.ts
RUN cp -r ../messages/JsonMessages/* src/Messages/JsonMessages
RUN yarn install && yarn run typesafe-i18n && yarn build
# 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
RUN rm dist/iframe.html
RUN yarn install
FROM thecodingmachine/nodejs:14-apache
COPY --from=builder --chown=docker:docker /usr/src/front/dist dist
COPY front/templater.sh .
USER root
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& apt-get install -y \
gettext-base \
&& rm -rf /var/lib/apt/lists/*
USER docker
ENV NODE_ENV=production
ENV STARTUP_COMMAND_0="./templater.sh"
ENV STARTUP_COMMAND_1="yarn run build"
ENV STARTUP_COMMAND_1="envsubst < dist/env-config.template.js > dist/env-config.js"
ENV APACHE_DOCUMENT_ROOT=dist/

View file

@ -1,4 +1,6 @@
index.html
index.tmpl.html.tmp
/js/
/fonts/
style.*.css
!env-config.template.js
*.png

27
front/dist/env-config.template.js vendored Normal file
View file

@ -0,0 +1,27 @@
window.env = {
SKIP_RENDER_OPTIMIZATIONS: '${SKIP_RENDER_OPTIMIZATIONS}',
DISABLE_NOTIFICATIONS: '${DISABLE_NOTIFICATIONS}',
PUSHER_URL: '${PUSHER_URL}',
UPLOADER_URL: '${UPLOADER_URL}',
ADMIN_URL: '${ADMIN_URL}',
CONTACT_URL: '${CONTACT_URL}',
PROFILE_URL: '${PROFILE_URL}',
ICON_URL: '${ICON_URL}',
DEBUG_MODE: '${DEBUG_MODE}',
STUN_SERVER: '${STUN_SERVER}',
TURN_SERVER: '${TURN_SERVER}',
TURN_USER: '${TURN_USER}',
TURN_PASSWORD: '${TURN_PASSWORD}',
JITSI_URL: '${JITSI_URL}',
JITSI_PRIVATE_MODE: '${JITSI_PRIVATE_MODE}',
START_ROOM_URL: '${START_ROOM_URL}',
MAX_USERNAME_LENGTH: '${MAX_USERNAME_LENGTH}',
MAX_PER_GROUP: '${MAX_PER_GROUP}',
DISPLAY_TERMS_OF_USE: '${DISPLAY_TERMS_OF_USE}',
POSTHOG_API_KEY: '${POSTHOG_API_KEY}',
POSTHOG_URL: '${POSTHOG_URL}',
NODE_ENV: '${NODE_ENV}',
DISABLE_ANONYMOUS: '${DISABLE_ANONYMOUS}',
OPID_LOGIN_SCREEN_PROVIDER: '${OPID_LOGIN_SCREEN_PROVIDER}',
FALLBACK_LOCALE: '${FALLBACK_LOCALE}',
};

68
front/dist/index.ejs vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,126 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- TRACK CODE -->
<!-- END TRACK CODE -->
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicons/favicon-16x16.png">
<link rel="manifest" href="static/images/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<base href="/">
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
<title>WorkAdventure</title>
</head>
<body id="body" style="margin: 0; background-color: #000">
<div class="main-container" id="main-container">
<!-- Create the editor container -->
<div id="game" class="game">
<div id="cowebsite-container">
<div id="cowebsite-container-main">
<div id="cowebsite-slot-1">
<div class="actions">
<button type="button" class="nes-btn is-primary expand">></button>
<button type="button" class="nes-btn is-error close">&times;</button>
</div>
</div>
</div>
<div id="cowebsite-container-sub">
<div id="cowebsite-slot-2">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
<div id="cowebsite-slot-3">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
<div id="cowebsite-slot-4">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
</div>
</div>
<div id="svelte-overlay"></div>
<div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section">
</div>
<aside id="sidebar" class="sidebar">
</aside>
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
</div>
</div>
</div>
<div id="cowebsite" class="cowebsite hidden">
<aside id="cowebsite-aside" class="noselect">
<div id="cowebsite-aside-buttons">
<button class="top-right-btn nes-btn is-error" id="cowebsite-close" alt="close all co-websites">
&times;
</button>
<button class="top-right-btn nes-btn is-primary" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
<img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
</button>
</div>
<div id="cowebsite-aside-holder">
<img src="/static/images/menu.svg" alt="hold to resize"/>
</div>
<div id="cowebsite-sub-icons"></div>
</aside>
<main id="cowebsite-slot-0"></main>
</div>
<div id="cowebsite-buffer"></div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>
</body>
</html>

View file

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><defs><image width="12" height="14" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAOAQMAAAAhc2+vAAAAAXNSR0IB2cksfwAAAAZQTFRFAAAA////pdmf3QAAAAJ0Uk5TAP9bkSK1AAAAJUlEQVR4nGNgOMDAoMDw/wMDCDgwMDQwQIAASBgE/j8ACRswAACLjwYPIknTggAAAABJRU5ErkJggg=="/><image width="12" height="12" id="img2" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAgMAAAArG7R0AAAAAXNSR0IB2cksfwAAAAlQTFRFAAAA/////wAAzV63nAAAAAN0Uk5TAP8BHlUJOgAAACJJREFUeJxjYGAQYGBgYGEIDQ0F0xA+BwMDEi80NASqigEAOD8CVqGVgwsAAAAASUVORK5CYII="/></defs><style></style><use href="#img1" x="2" y="1" /><use href="#img2" x="2" y="2" /></svg>

After

Width:  |  Height:  |  Size: 717 B

View file

@ -15,6 +15,7 @@
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"css-loader": "^5.2.4",
"css-minimizer-webpack-plugin": "^3.3.1",
"eslint": "^8.4.1",
"eslint-plugin-svelte3": "^3.2.1",
"fork-ts-checker-webpack-plugin": "^6.5.0",

View file

@ -6,13 +6,14 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface()
allowApi: tg.isOptional(tg.isBoolean),
allowPolicy: tg.isOptional(tg.isString),
position: tg.isOptional(tg.isNumber),
closable: tg.isOptional(tg.isBoolean),
lazy: tg.isOptional(tg.isBoolean),
})
.get();
export const isCoWebsite = new tg.IsInterface()
.withProperties({
id: tg.isString,
position: tg.isNumber,
})
.get();

View file

@ -8,7 +8,6 @@ import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import { scriptUtils } from "./ScriptUtils";
import { isGoToPageEvent } from "./Events/GoToPageEvent";
import { isCloseCoWebsite, CloseCoWebsiteEvent } from "./Events/CloseCoWebsiteEvent";
import {
IframeErrorAnswerEvent,
IframeQueryMap,

View file

@ -1,4 +1,3 @@
import { coWebsiteManager, CoWebsite } from "../WebRtc/CoWebsiteManager";
import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore } from "../Stores/ChatStore";
import type { ChatEvent } from "./Events/ChatEvent";

View file

@ -1,7 +1,7 @@
import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution";
export class CoWebsite {
constructor(private readonly id: string, public readonly position: number) {}
constructor(private readonly id: string) {}
close() {
return queryWorkadventure({
@ -41,7 +41,14 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
});
}
async openCoWebSite(url: string, allowApi?: boolean, allowPolicy?: string, position?: number): Promise<CoWebsite> {
async openCoWebSite(
url: string,
allowApi?: boolean,
allowPolicy?: string,
position?: number,
closable?: boolean,
lazy?: boolean
): Promise<CoWebsite> {
const result = await queryWorkadventure({
type: "openCoWebsite",
data: {
@ -49,9 +56,11 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
allowApi,
allowPolicy,
position,
closable,
lazy,
},
});
return new CoWebsite(result.id, result.position);
return new CoWebsite(result.id);
}
async getCoWebSites(): Promise<CoWebsite[]> {
@ -59,7 +68,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
type: "getCoWebsites",
data: undefined,
});
return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id, cowebsiteEvent.position));
return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id));
}
/**

View file

@ -1,171 +1,52 @@
<script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import { menuIconVisiblilityStore, menuVisiblilityStore } from "../Stores/MenuStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import { errorStore } from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte";
import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import { requestVisitCardsStore } from "../Stores/GameStore";
import type { Game } from "../Phaser/Game/Game";
import { chatVisibilityStore } from "../Stores/ChatStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import { errorStore } from "../Stores/ErrorStore";
import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import Chat from "./Chat/Chat.svelte";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import MainLayout from "./MainLayout.svelte";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
import ActionsMenu from './ActionsMenu/ActionsMenu.svelte';
import { actionsMenuStore } from '../Stores/ActionsMenuStore';
export let game: Game;
</script>
<div>
{#if $loginSceneVisibleStore}
<div class="scrollable">
<LoginScene {game} />
</div>
{/if}
{#if $selectCharacterSceneVisibleStore}
<div>
<SelectCharacterScene {game} />
</div>
{/if}
{#if $customCharacterSceneVisibleStore}
<div>
<CustomCharacterScene {game} />
</div>
{/if}
{#if $selectCompanionSceneVisibleStore}
<div>
<SelectCompanionScene {game} />
</div>
{/if}
{#if $enableCameraSceneVisibilityStore}
<div class="scrollable">
<EnableCameraScene {game} />
</div>
{/if}
{#if $banMessageStore.length > 0}
<div>
<BanMessageContainer />
</div>
{:else if $textMessageStore.length > 0}
<div>
<TextMessageContainer />
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
{#if $audioManagerVisibilityStore}
<div>
<AudioManager />
</div>
{/if}
{#if $layoutManagerVisibilityStore}
<div>
<LayoutManager />
</div>
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<div>
<ReportMenu />
</div>
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<div>
<FollowMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon />
</div>
{/if}
{#if $menuVisiblilityStore}
<div>
<Menu />
</div>
{/if}
{#if $emoteMenuStore}
<div>
<EmoteMenu />
</div>
{/if}
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay />
<MyCamera />
<CameraControls />
</div>
{/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup />
</div>
{/if}
{#if $showLimitRoomModalStore}
<div>
<LimitRoomModal />
</div>
{/if}
{#if $showShareLinkMapModalStore}
<div>
<ShareLinkMapModal />
</div>
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $actionsMenuStore}
<ActionsMenu />
{/if}
{#if $errorStore.length > 0}
<div>
<ErrorDialog />
</div>
{/if}
{#if $errorStore.length > 0}
<div>
<ErrorDialog />
</div>
{:else if $loginSceneVisibleStore}
<div class="scrollable">
<LoginScene {game} />
</div>
{:else if $selectCharacterSceneVisibleStore}
<div>
<SelectCharacterScene {game} />
</div>
{:else if $customCharacterSceneVisibleStore}
<div>
<CustomCharacterScene {game} />
</div>
{:else if $selectCompanionSceneVisibleStore}
<div>
<SelectCompanionScene {game} />
</div>
{:else if $enableCameraSceneVisibilityStore}
<div class="scrollable">
<EnableCameraScene {game} />
</div>
{:else}
<MainLayout />
{#if $chatVisibilityStore}
<Chat />
{/if}
{#if $warningContainerStore}
<WarningContainer />
{/if}
</div>
{/if}

View file

@ -157,13 +157,16 @@
<style lang="scss">
div.main-audio-manager.nes-container.is-rounded {
position: relative;
top: 0.5rem;
position: absolute;
top: 1%;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw);
padding: 3px 3px;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
z-index: 550;
background-color: rgb(0, 0, 0, 0.5);
display: grid;

View file

@ -9,10 +9,15 @@
import microphoneCloseImg from "./images/microphone-close.svg";
import layoutPresentationImg from "./images/layout-presentation.svg";
import layoutChatImg from "./images/layout-chat.svg";
import { layoutModeStore } from "../Stores/StreamableCollectionStore";
import followImg from "./images/follow.svg";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { peerStore } from "../Stores/PeerStore";
import { onDestroy } from "svelte";
import { embedScreenLayout } from "../Stores/EmbedScreensStore";
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
import { gameManager } from "../Phaser/Game/GameManager";
const gameScene = gameManager.getCurrentGameScene();
function screenSharingClick(): void {
if (isSilent) return;
@ -42,10 +47,26 @@
}
function switchLayoutMode() {
if ($layoutModeStore === LayoutMode.Presentation) {
$layoutModeStore = LayoutMode.VideoChat;
if ($embedScreenLayout === LayoutMode.Presentation) {
$embedScreenLayout = LayoutMode.VideoChat;
} else {
$layoutModeStore = LayoutMode.Presentation;
$embedScreenLayout = LayoutMode.Presentation;
}
}
function followClick() {
switch ($followStateStore) {
case "off":
gameScene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
break;
case "requesting":
case "active":
case "ending":
gameScene.connection?.emitFollowAbort();
followUsersStore.stopFollowing();
break;
}
}
@ -56,40 +77,162 @@
onDestroy(unsubscribeIsSilent);
</script>
<div>
<div class="btn-cam-action">
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
{#if $layoutModeStore === LayoutMode.Presentation}
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" />
{:else}
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" />
{/if}
</div>
<div
class="btn-monitor"
on:click={screenSharingClick}
class:hide={!$screenSharingAvailableStore || isSilent}
class:enabled={$requestedScreenSharingState}
>
{#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing" />
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing" />
{/if}
</div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{#if $requestedCameraState && !isSilent}
<img src={cinemaImg} alt="Turn on webcam" />
{:else}
<img src={cinemaCloseImg} alt="Turn off webcam" />
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone" />
{:else}
<img src={microphoneCloseImg} alt="Turn off microphone" />
{/if}
</div>
<div class="btn-cam-action">
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
{#if $embedScreenLayout === LayoutMode.Presentation}
<img class="noselect" src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" />
{:else}
<img class="noselect" src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" />
{/if}
</div>
<div
class="btn-follow"
class:hide={($peerStore.size === 0 && $followStateStore === "off") || isSilent}
class:disabled={$followStateStore !== "off"}
on:click={followClick}
>
<img class="noselect" src={followImg} alt="" />
</div>
<div
class="btn-monitor"
on:click={screenSharingClick}
class:hide={!$screenSharingAvailableStore || isSilent}
class:enabled={$requestedScreenSharingState}
>
{#if $requestedScreenSharingState && !isSilent}
<img class="noselect" src={monitorImg} alt="Start screen sharing" />
{:else}
<img class="noselect" src={monitorCloseImg} alt="Stop screen sharing" />
{/if}
</div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
{#if $requestedCameraState && !isSilent}
<img class="noselect" src={cinemaImg} alt="Turn on webcam" />
{:else}
<img class="noselect" src={cinemaCloseImg} alt="Turn off webcam" />
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img class="noselect" src={microphoneImg} alt="Turn on microphone" />
{:else}
<img class="noselect" src={microphoneCloseImg} alt="Turn off microphone" />
{/if}
</div>
</div>
<style lang="scss">
@import "../../style/breakpoints.scss";
.btn-cam-action {
pointer-events: all;
position: absolute;
display: inline-flex;
bottom: 10px;
right: 15px;
width: 360px;
height: 40px;
text-align: center;
align-content: center;
justify-content: flex-end;
z-index: 251;
&:hover {
div.hide {
transform: translateY(60px);
}
}
}
/*btn animation*/
.btn-cam-action div {
cursor: url("../../style/images/cursor_pointer.png"), pointer;
display: flex;
align-items: center;
justify-content: center;
border: solid 0px black;
width: 44px;
height: 44px;
background: #666;
box-shadow: 2px 2px 24px #444;
border-radius: 48px;
transform: translateY(15px);
transition-timing-function: ease-in-out;
transition: all 0.3s;
margin: 0 4%;
&.hide {
transform: translateY(60px);
}
}
.btn-cam-action div.disabled {
background: #d75555;
}
.btn-cam-action div.enabled {
background: #73c973;
}
.btn-cam-action:hover div {
transform: translateY(0);
}
.btn-cam-action div:hover {
background: #407cf7;
box-shadow: 4px 4px 48px #666;
transition: 120ms;
}
.btn-micro {
pointer-events: auto;
}
.btn-video {
pointer-events: auto;
transition: all 0.25s;
}
.btn-monitor {
pointer-events: auto;
}
.btn-layout {
pointer-events: auto;
transition: all 0.15s;
}
.btn-cam-action div img {
height: 22px;
width: 30px;
position: relative;
cursor: url("../../style/images/cursor_pointer.png"), pointer;
}
.btn-follow {
pointer-events: auto;
img {
filter: brightness(0) invert(1);
}
}
@media (hover: none) {
/**
* If we cannot hover over elements, let's display camera button in full.
*/
.btn-cam-action {
div {
transform: translateY(0px);
}
}
}
@include media-breakpoint-up(sm) {
.btn-cam-action {
right: 0;
width: 100%;
height: 40%;
max-height: 40px;
div {
width: 20%;
max-height: 44px;
}
}
}
</style>

View file

@ -43,7 +43,7 @@
<svelte:window on:keydown={onKeyDown} on:click={onClick} />
<aside class="chatWindow" transition:fly={{ x: -1000, duration: 500 }} bind:this={chatWindowElement}>
<p class="close-icon" on:click={closeChat}>&times</p>
<p class="close-icon noselect" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
<li><p class="system-text">{$LL.chat.intro()}</p></li>
@ -78,7 +78,7 @@
}
aside.chatWindow {
z-index: 100;
z-index: 1000;
pointer-events: auto;
position: absolute;
top: 0;

View file

@ -83,6 +83,8 @@
</form>
<style lang="scss">
@import "../../../style/breakpoints.scss";
form.customCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
@ -129,7 +131,7 @@
}
}
@media only screen and (max-width: 800px) {
@include media-breakpoint-up(md) {
form.customCharacterScene button.customCharacterSceneButtonLeft {
left: 5vw;
}

View file

@ -0,0 +1,32 @@
<script lang="typescript">
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import MediaBox from "../Video/MediaBox.svelte";
export let highlightedEmbedScreen: EmbedScreen | null;
export let full = false;
$: clickable = !full;
</script>
<aside class="cameras-container" class:full>
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if !highlightedEmbedScreen || highlightedEmbedScreen.type !== "streamable" || (highlightedEmbedScreen.type === "streamable" && highlightedEmbedScreen.embed !== peer)}
<MediaBox streamable={peer} isClickable={clickable} />
{/if}
{/each}
</aside>
<style lang="scss">
.cameras-container {
flex: 0 0 25%;
overflow-y: auto;
overflow-x: hidden;
&:first-child {
margin-top: 2%;
}
&.full {
flex: 0 0 100%;
}
}
</style>

View file

@ -0,0 +1,272 @@
<script lang="typescript">
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { coWebsitesNotAsleep, mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
export let index: number;
export let coWebsite: CoWebsite;
export let vertical: boolean;
let icon: HTMLImageElement;
let iconLoaded = false;
let state = coWebsite.state;
const coWebsiteUrl = coWebsite.iframe.src;
const urlObject = new URL(coWebsiteUrl);
onMount(() => {
icon.src = `${ICON_URL}/icon?url=${urlObject.hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = urlObject.hostname;
icon.onload = () => {
iconLoaded = true;
};
});
async function onClick() {
if (vertical) {
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
if ($mainCoWebsite.iframe.id === coWebsite.iframe.id) {
const coWebsites = $coWebsitesNotAsleep;
const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined;
if (newMain) {
coWebsiteManager.goToMain(coWebsite);
}
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
}
}
if ($state === "asleep") {
await coWebsiteManager.loadCoWebsite(coWebsite);
}
coWebsiteManager.resizeAllIframes();
}
function noDrag() {
return false;
}
let isHighlight: boolean = false;
let isMain: boolean = false;
$: {
isMain = $mainCoWebsite !== undefined && $mainCoWebsite.iframe === coWebsite.iframe;
isHighlight =
$highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" &&
$highlightedEmbedScreen.embed.iframe === coWebsite.iframe;
}
</script>
<div
id={"cowebsite-thumbnail-" + index}
class="cowebsite-thumbnail nes-pointer"
class:asleep={$state === "asleep"}
class:loading={$state === "loading"}
class:ready={$state === "ready"}
class:displayed={isMain || isHighlight}
class:vertical
on:click={onClick}
>
<img
class="cowebsite-icon noselect nes-pointer"
class:hide={!iconLoaded}
bind:this={icon}
on:dragstart|preventDefault={noDrag}
alt=""
/>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="cowebsite-icon"
class:hide={iconLoaded}
style="margin: auto; background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%; shape-rendering: auto;"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<rect x="19" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0s"
calcMode="discrete"
/>
</rect><rect x="40" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.125s"
calcMode="discrete"
/>
</rect><rect x="61" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.25s"
calcMode="discrete"
/>
</rect><rect x="19" y="40" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.875s"
calcMode="discrete"
/>
</rect><rect x="61" y="40" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.375s"
calcMode="discrete"
/>
</rect><rect x="19" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.75s"
calcMode="discrete"
/>
</rect><rect x="40" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.625s"
calcMode="discrete"
/>
</rect><rect x="61" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.5s"
calcMode="discrete"
/>
</rect>
</svg>
</div>
<style lang="scss">
.cowebsite-thumbnail {
position: relative;
padding: 0;
background-color: rgba(#000000, 0.6);
margin: 12px;
margin-top: auto;
margin-bottom: auto;
&::before {
content: "";
position: absolute;
width: 58px;
height: 58px;
left: -8px;
top: -8px;
margin: 4px;
border-style: solid;
border-width: 4px;
border-image-slice: 3;
border-image-width: 3;
border-image-repeat: stretch;
border-image-source: url('data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" ?><svg version="1.1" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M3 1 h1 v1 h-1 z M4 1 h1 v1 h-1 z M2 2 h1 v1 h-1 z M5 2 h1 v1 h-1 z M1 3 h1 v1 h-1 z M6 3 h1 v1 h-1 z M1 4 h1 v1 h-1 z M6 4 h1 v1 h-1 z M2 5 h1 v1 h-1 z M5 5 h1 v1 h-1 z M3 6 h1 v1 h-1 z M4 6 h1 v1 h-1 z" fill="rgb(33,37,41)" /></svg>');
border-image-outset: 1;
}
&.vertical {
margin: 7px;
&::before {
width: 48px;
height: 48px;
}
.cowebsite-icon {
width: 40px;
height: 40px;
}
}
&.displayed {
&:not(.vertical) {
animation: activeThumbnail 300ms ease-in 0s forwards;
}
}
&.asleep {
filter: grayscale(100%);
--webkit-filter: grayscale(100%);
}
&.loading {
animation: 2500ms ease-in-out 0s infinite alternate backgroundLoading;
}
&.ready {
&::before {
border-image-source: url('data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" ?><svg version="1.1" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M3 1 h1 v1 h-1 z M4 1 h1 v1 h-1 z M2 2 h1 v1 h-1 z M5 2 h1 v1 h-1 z M1 3 h1 v1 h-1 z M6 3 h1 v1 h-1 z M1 4 h1 v1 h-1 z M6 4 h1 v1 h-1 z M2 5 h1 v1 h-1 z M5 5 h1 v1 h-1 z M3 6 h1 v1 h-1 z M4 6 h1 v1 h-1 z" fill="rgb(38, 74, 110)" /></svg>');
}
}
@keyframes backgroundLoading {
0% {
background-color: rgba(#000000, 0.6);
}
100% {
background-color: #25598e;
}
}
@keyframes activeThumbnail {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-15px);
}
}
.cowebsite-icon {
width: 50px;
height: 50px;
object-fit: cover;
&.hide {
display: none;
}
}
}
</style>

View file

@ -0,0 +1,42 @@
<script lang="typescript">
import { coWebsites } from "../../Stores/CoWebsiteStore";
import CoWebsiteThumbnail from "./CoWebsiteThumbnailSlot.svelte";
export let vertical = false;
</script>
{#if $coWebsites.length > 0}
<div id="cowebsite-thumbnail-container" class:vertical>
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.iframe.id)}
<CoWebsiteThumbnail {index} {coWebsite} {vertical} />
{/each}
</div>
{/if}
<style lang="scss">
#cowebsite-thumbnail-container {
pointer-events: all;
height: 100px;
width: 100%;
display: flex;
position: absolute;
bottom: 5px;
left: 2%;
overflow-x: auto;
overflow-y: hidden;
&.vertical {
height: auto !important;
width: auto !important;
bottom: auto !important;
left: auto !important;
position: relative;
overflow-x: hidden;
overflow-y: auto;
flex-direction: column;
align-items: center;
padding-top: 4px;
padding-bottom: 4px;
}
}
</style>

View file

@ -0,0 +1,22 @@
<script lang="typescript">
import PresentationLayout from "./Layouts/PresentationLayout.svelte";
import MozaicLayout from "./Layouts/MozaicLayout.svelte";
import { LayoutMode } from "../../WebRtc/LayoutManager";
import { embedScreenLayout } from "../../Stores/EmbedScreensStore";
</script>
<div id="embedScreensContainer">
{#if $embedScreenLayout === LayoutMode.Presentation}
<PresentationLayout />
{:else}
<MozaicLayout />
{/if}
</div>
<style lang="scss">
#embedScreensContainer {
display: flex;
padding-top: 2%;
height: 100%;
}
</style>

View file

@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from "svelte";
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import { streamableCollectionStore } from "../../../Stores/StreamableCollectionStore";
import MediaBox from "../../Video/MediaBox.svelte";
let layoutDom: HTMLDivElement;
const resizeObserver = new ResizeObserver(() => {});
onMount(() => {
resizeObserver.observe(layoutDom);
highlightedEmbedScreen.removeHighlight();
});
</script>
<div id="mozaic-layout" bind:this={layoutDom}>
<div
class="media-container"
class:full-width={$streamableCollectionStore.size === 1 || $streamableCollectionStore.size === 2}
class:quarter={$streamableCollectionStore.size === 3 || $streamableCollectionStore.size === 4}
>
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
<MediaBox
streamable={peer}
mozaicFullWidth={$streamableCollectionStore.size === 1 || $streamableCollectionStore.size === 2}
mozaicQuarter={$streamableCollectionStore.size === 3 || $streamableCollectionStore.size === 4}
/>
{/each}
</div>
</div>
<style lang="scss">
#mozaic-layout {
height: 100%;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
.media-container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 33.3% 33.3% 33.3%;
align-items: center;
justify-content: center;
overflow-y: auto;
overflow-x: hidden;
&.full-width {
grid-template-columns: 100%;
grid-template-rows: 50% 50%;
}
&.quarter {
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
}
}
}
</style>

View file

@ -0,0 +1,143 @@
<script lang="ts">
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import CamerasContainer from "../CamerasContainer.svelte";
import MediaBox from "../../Video/MediaBox.svelte";
import { coWebsiteManager } from "../../../WebRtc/CoWebsiteManager";
import { afterUpdate, onMount } from "svelte";
import { isMediaBreakpointDown, isMediaBreakpointUp } from "../../../Utils/BreakpointsUtils";
import { peerStore } from "../../../Stores/PeerStore";
function closeCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
if ($highlightedEmbedScreen.embed.closable) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted closing");
});
} else {
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted unloading");
});
}
}
}
afterUpdate(() => {
if ($highlightedEmbedScreen) {
coWebsiteManager.resizeAllIframes();
}
});
let layoutDom: HTMLDivElement;
let displayCoWebsiteContainer = isMediaBreakpointDown("lg");
let displayFullMedias = isMediaBreakpointUp("sm");
const resizeObserver = new ResizeObserver(() => {
displayCoWebsiteContainer = isMediaBreakpointDown("lg");
displayFullMedias = isMediaBreakpointUp("sm");
if (!displayCoWebsiteContainer && $highlightedEmbedScreen && $highlightedEmbedScreen.type === "cowebsite") {
highlightedEmbedScreen.removeHighlight();
}
if (displayFullMedias) {
highlightedEmbedScreen.removeHighlight();
}
});
onMount(() => {
resizeObserver.observe(layoutDom);
});
</script>
<div id="presentation-layout" bind:this={layoutDom} class:full-medias={displayFullMedias}>
{#if displayFullMedias}
<div id="full-medias">
<CamerasContainer full={true} highlightedEmbedScreen={$highlightedEmbedScreen} />
</div>
{:else}
<div id="embed-left-block" class:full={$peerStore.size === 0}>
<div id="main-embed-screen">
{#if $highlightedEmbedScreen}
{#if $highlightedEmbedScreen.type === "streamable"}
{#key $highlightedEmbedScreen.embed.uniqueId}
<MediaBox
isHightlighted={true}
isClickable={true}
streamable={$highlightedEmbedScreen.embed}
/>
{/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.iframe.id}
<div
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.iframe.id}
class="highlighted-cowebsite nes-container is-rounded"
>
<div class="actions">
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
>&times;</button
>
</div>
</div>
{/key}
{/if}
{/if}
</div>
</div>
{#if $peerStore.size > 0}
<CamerasContainer highlightedEmbedScreen={$highlightedEmbedScreen} />
{/if}
{/if}
</div>
<style lang="scss">
#presentation-layout {
height: 100%;
width: 100%;
display: flex;
&.full-medias {
overflow-y: auto;
overflow-x: hidden;
}
}
#embed-left-block {
display: flex;
flex-direction: column;
flex: 0 0 75%;
height: 100%;
width: 75%;
&.full {
flex: 0 0 98% !important;
width: 98% !important;
}
}
#main-embed-screen {
height: 100%;
margin-bottom: 3%;
.highlighted-cowebsite {
height: 100% !important;
width: 96%;
background-color: rgba(#000000, 0.6);
margin: 0 !important;
.actions {
z-index: 200;
position: relative;
display: flex;
flex-direction: row;
justify-content: end;
gap: 2%;
button {
pointer-events: all;
}
}
}
}
</style>

View file

@ -3,7 +3,8 @@
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { onDestroy, onMount } from "svelte";
import { EmojiButton } from "@joeattardi/emoji-button";
import { isMobile } from "../../Enum/EnvironmentVariable";
import LL from "../../i18n/i18n-svelte";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
let emojiContainer: HTMLElement;
let picker: EmojiButton;
@ -15,10 +16,31 @@
rootElement: emojiContainer,
styleProperties: {
"--font": "Press Start 2P",
"--text-color": "whitesmoke",
"--secondary-text-color": "whitesmoke",
"--category-button-color": "whitesmoke",
},
emojisPerRow: isMobile() ? 6 : 8,
emojisPerRow: isMediaBreakpointUp("md") ? 6 : 8,
autoFocusSearch: false,
style: "twemoji",
showPreview: false,
i18n: {
search: $LL.emoji.search(),
categories: {
recents: $LL.emoji.categories.recents(),
smileys: $LL.emoji.categories.smileys(),
people: $LL.emoji.categories.people(),
animals: $LL.emoji.categories.animals(),
food: $LL.emoji.categories.food(),
activities: $LL.emoji.categories.activities(),
travel: $LL.emoji.categories.travel(),
objects: $LL.emoji.categories.objects(),
symbols: $LL.emoji.categories.symbols(),
flags: $LL.emoji.categories.flags(),
custom: $LL.emoji.categories.custom(),
},
notFound: $LL.emoji.notFound(),
},
});
//the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100);
@ -64,6 +86,8 @@
height: 100%;
justify-content: center;
align-items: center;
position: absolute;
z-index: 300;
}
.emote-menu {

View file

@ -127,6 +127,8 @@
</form>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.enableCameraScene {
pointer-events: auto;
margin: 20px auto 0;
@ -214,7 +216,7 @@
}
}
@media only screen and (max-width: 800px) {
@include media-breakpoint-up(md) {
.enableCameraScene h2 {
font-size: 80%;
}

View file

@ -0,0 +1,33 @@
<script lang="typescript">
import followImg from "../images/follow.svg";
export let hidden: Boolean;
let cancelButton = false;
</script>
<div class="btn-follow" class:hide={hidden} class:cancel={cancelButton}>
<img src={followImg} alt="" />
</div>
<style lang="scss">
.btn-follow {
cursor: url("../../../style/images/cursor_pointer.png"), pointer;
display: flex;
align-items: center;
justify-content: center;
border: solid 0px black;
width: 44px;
height: 44px;
background: #666;
box-shadow: 2px 2px 24px #444;
border-radius: 48px;
transform: translateY(15px);
transition-timing-function: ease-in-out;
margin: 0 4%;
img {
filter: brightness(0) invert(1);
}
}
</style>

View file

@ -1,9 +1,5 @@
<!--
vim: ft=typescript
-->
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
import LL from "../../i18n/i18n-svelte";
@ -14,10 +10,6 @@ vim: ft=typescript
return user ? user.playerName : "";
}
function sendFollowRequest() {
gameScene.CurrentPlayer.sendFollowRequest();
}
function acceptFollowRequest() {
gameScene.CurrentPlayer.startFollowing();
}
@ -83,7 +75,7 @@ vim: ft=typescript
{#if $followStateStore === "active" || $followStateStore === "ending"}
<div class="interact-status nes-container is-rounded">
<section class="interact-status">
<section>
{#if $followRoleStore === "follower"}
<p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 0}
@ -109,48 +101,27 @@ vim: ft=typescript
</div>
{/if}
{#if $followStateStore === "off"}
<button
type="button"
class="nes-btn is-primary follow-menu-button"
on:click|preventDefault={sendFollowRequest}
title="Ask others to follow"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
{#if $followRoleStore === "follower"}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop following"><img class="background-img" src={followImg} alt="" /></button
>
{:else}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop leading the way"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{/if}
<style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container {
padding: 5px;
}
div.interact-status {
.interact-status {
background-color: #333333;
color: whitesmoke;
position: relative;
height: 2.7em;
position: absolute;
max-height: 2.7em;
width: 40vw;
top: 87vh;
margin: auto;
text-align: center;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 400;
}
div.interact-menu {
@ -158,10 +129,14 @@ vim: ft=typescript
background-color: #333333;
color: whitesmoke;
position: relative;
position: absolute;
width: 60vw;
top: 60vh;
margin: auto;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 150;
section.interact-menu-title {
margin-bottom: 20px;
@ -189,23 +164,16 @@ vim: ft=typescript
}
}
.follow-menu-button {
position: absolute;
bottom: 10px;
left: 10px;
pointer-events: all;
}
@media only screen and (max-width: 800px) {
div.interact-status {
width: 100vw;
@include media-breakpoint-up(md) {
.interact-status {
width: 90vw;
top: 78vh;
font-size: 0.75em;
}
div.interact-menu {
height: 21vh;
width: 100vw;
max-height: 21vh;
width: 90vw;
font-size: 0.75em;
}
}

View file

@ -22,7 +22,7 @@
<form
class="helpCameraSettings nes-container"
on:submit|preventDefault={close}
transition:fly={{ y: -900, duration: 500 }}
transition:fly={{ y: -50, duration: 500 }}
>
<section>
<h2>{$LL.camera.help.title()}</h2>
@ -55,9 +55,12 @@
background: #eceeee;
margin-left: auto;
margin-right: auto;
margin-top: 10vh;
left: 0;
right: 0;
margin-top: 4%;
max-height: 80vh;
max-width: 80vw;
z-index: 600;
overflow: auto;
text-align: center;

View file

@ -21,9 +21,11 @@
left: 0;
right: 0;
bottom: 40px;
margin: 0 auto;
margin-right: auto;
margin-left: auto;
padding: 0;
width: clamp(200px, 20vw, 20vw);
z-index: 155;
display: flex;
flex-direction: column;
@ -31,6 +33,10 @@
animation: moveMessage 0.5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
div {
margin-bottom: 5%;
}
}
div.nes-container.is-rounded {

View file

@ -0,0 +1,176 @@
<script lang="typescript">
import { onMount } from "svelte";
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import { embedScreenLayout, hasEmbedScreen } from "../Stores/EmbedScreensStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility";
import { requestVisitCardsStore } from "../Stores/GameStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import { layoutManagerActionVisibilityStore } from "../Stores/LayoutManagerStore";
import { menuIconVisiblilityStore, menuVisiblilityStore, warningContainerStore } from "../Stores/MenuStore";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import AudioManager from "./AudioManager/AudioManager.svelte";
import CameraControls from "./CameraControls.svelte";
import EmbedScreensContainer from "./EmbedScreens/EmbedScreensContainer.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import LayoutActionManager from "./LayoutActionManager/LayoutActionManager.svelte";
import Menu from "./Menu/Menu.svelte";
import MenuIcon from "./Menu/MenuIcon.svelte";
import MyCamera from "./MyCamera.svelte";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { isMediaBreakpointDown, isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
import CoWebsitesContainer from "./EmbedScreens/CoWebsitesContainer.svelte";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { actionsMenuStore } from '../Stores/ActionsMenuStore';
import ActionsMenu from './ActionsMenu/ActionsMenu.svelte';
let mainLayout: HTMLDivElement;
let displayCoWebsiteContainerMd = isMediaBreakpointUp("md");
let displayCoWebsiteContainerLg = isMediaBreakpointDown("lg");
const resizeObserver = new ResizeObserver(() => {
displayCoWebsiteContainerMd = isMediaBreakpointUp("md");
displayCoWebsiteContainerLg = isMediaBreakpointDown("lg");
});
onMount(() => {
resizeObserver.observe(mainLayout);
});
</script>
<div id="main-layout" bind:this={mainLayout}>
<aside id="main-layout-left-aside">
{#if $menuIconVisiblilityStore}
<MenuIcon />
{/if}
{#if $embedScreenLayout === LayoutMode.VideoChat || displayCoWebsiteContainerMd}
<CoWebsitesContainer vertical={true} />
{/if}
</aside>
<section id="main-layout-main">
{#if $menuVisiblilityStore}
<Menu />
{/if}
{#if $banMessageStore.length > 0}
<BanMessageContainer />
{:else if $textMessageStore.length > 0}
<TextMessageContainer />
{/if}
{#if $soundPlayingStore}
<AudioPlaying url={$soundPlayingStore} />
{/if}
{#if $warningContainerStore}
<WarningContainer />
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<ReportMenu />
{/if}
{#if $helpCameraSettingsVisibleStore}
<HelpCameraSettingsPopup />
{/if}
{#if $audioManagerVisibilityStore}
<AudioManager />
{/if}
{#if $showLimitRoomModalStore}
<LimitRoomModal />
{/if}
{#if $showShareLinkMapModalStore}
<ShareLinkMapModal />
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<FollowMenu />
{/if}
{#if $actionsMenuStore}
<ActionsMenu />
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $emoteMenuStore}
<EmoteMenu />
{/if}
{#if hasEmbedScreen}
<EmbedScreensContainer />
{/if}
</section>
<section id="main-layout-baseline">
{#if displayCoWebsiteContainerLg}
<CoWebsitesContainer />
{/if}
{#if $layoutManagerActionVisibilityStore}
<LayoutActionManager />
{/if}
{#if $myCameraVisibilityStore}
<MyCamera />
<CameraControls />
{/if}
</section>
</div>
<style lang="scss">
@import "../../style/breakpoints.scss";
#main-layout {
display: grid;
grid-template-columns: 120px calc(100% - 120px);
grid-template-rows: 80% 20%;
&-left-aside {
min-width: 80px;
}
&-baseline {
grid-column: 1/3;
}
}
@include media-breakpoint-up(md) {
#main-layout {
grid-template-columns: 15% 85%;
&-left-aside {
min-width: auto;
}
}
}
@include media-breakpoint-up(sm) {
#main-layout {
grid-template-columns: 20% 80%;
}
}
</style>

View file

@ -100,6 +100,8 @@
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.string-HTML {
white-space: pre-line;
}
@ -126,7 +128,7 @@
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
@include media-breakpoint-up(md) {
div.about-room-main {
section.container-overflow {
height: calc(100% - 120px);

View file

@ -67,6 +67,8 @@
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
div.global-message-main {
height: calc(100% - 50px);
display: grid;
@ -109,7 +111,7 @@
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
@include media-breakpoint-up(md) {
.global-message-content {
height: calc(100% - 5px);
}

View file

@ -36,6 +36,8 @@
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
div.guest-main {
height: calc(100% - 56px);
@ -57,7 +59,7 @@
}
}
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
@include media-breakpoint-up(md) {
div.guest-main {
section.share-url.not-mobile {
display: none;

View file

@ -125,6 +125,8 @@
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container {
padding: 5px;
}
@ -136,11 +138,15 @@
pointer-events: auto;
height: 80%;
width: 75%;
top: 10%;
top: 4%;
position: relative;
z-index: 80;
margin: auto;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
position: absolute;
z-index: 900;
display: grid;
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
@ -173,12 +179,12 @@
}
}
@media only screen and (max-width: 800px) {
@include media-breakpoint-up(md) {
div.menu-container-main {
--size-first-columns-grid: 120px;
height: 70%;
top: 55px;
width: 100%;
width: 95%;
font-size: 0.5em;
div.menu-nav-sidebar {

View file

@ -14,6 +14,7 @@
function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore));
}
function showChat() {
chatVisibilityStore.set(true);
}
@ -21,72 +22,97 @@
function register() {
window.open(`${ADMIN_URL}/second-step-register`, "_self");
}
function showInvite() {
showShareLinkMapModalStore.set(true);
}
function noDrag() {
return false;
}
</script>
<svelte:window />
<main class="menuIcon">
<main class="menuIcon noselect">
{#if $limitMapStore}
<img
src={logoInvite}
alt={$LL.menu.icon.open.invite()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showInvite}
/>
<img
src={logoRegister}
alt={$LL.menu.icon.open.register()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={register}
/>
{:else}
<img src={logoWA} alt={$LL.menu.icon.open.menu()} class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt={$LL.menu.icon.open.chat()} class="nes-pointer" on:click|preventDefault={showChat} />
<img
src={logoWA}
alt={$LL.menu.icon.open.menu()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showMenu}
/>
<img
src={logoTalk}
alt={$LL.menu.icon.open.chat()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showChat}
/>
{/if}
</main>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.menuIcon {
display: inline-grid;
z-index: 90;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20%;
z-index: 800;
position: relative;
margin: 25px;
img {
pointer-events: auto;
width: 60px;
padding-top: 0;
margin: 3px;
image-rendering: pixelated;
margin: 5%;
}
}
.menuIcon img:hover {
transform: scale(1.2);
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
@include media-breakpoint-up(sm) {
.menuIcon {
display: inline-grid;
z-index: 90;
position: relative;
margin: 25px;
margin-top: 10%;
img {
pointer-events: auto;
width: 60px;
padding-top: 0;
margin: 3px;
}
}
.menuIcon img:hover {
transform: scale(1.2);
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.menuIcon {
margin: 3px;
img {
width: 50px;
}
}
@include media-breakpoint-up(md) {
.menuIcon {
img {
width: 50px;
}
}
}

View file

@ -102,6 +102,8 @@
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
div.customize-main {
width: 100%;
display: inline-flex;
@ -161,7 +163,7 @@
}
}
@media only screen and (max-width: 800px) {
@include media-breakpoint-up(md) {
div.customize-main.content section button {
width: 130px;
}

View file

@ -2,11 +2,11 @@
import { localUserStore } from "../../Connexion/LocalUserStore";
import { videoConstraintStore } from "../../Stores/MediaStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { menuVisiblilityStore } from "../../Stores/MenuStore";
import LL, { locale } from "../../i18n/i18n-svelte";
import type { Locales } from "../../i18n/i18n-types";
import { displayableLocales, setCurrentLocale } from "../../i18n/locales";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted";
@ -85,6 +85,8 @@
function closeMenu() {
menuVisiblilityStore.set(false);
}
const isMobile = isMediaBreakpointUp("md");
</script>
<div class="settings-main" on:submit|preventDefault={saveSetting}>
@ -93,25 +95,25 @@
<div class="nes-select is-dark">
<select bind:value={valueGame}>
<option value={120}
>{isMobile()
>{isMobile
? $LL.menu.settings.gameQuality.short.high()
: $LL.menu.settings.gameQuality.long.high()}</option
>
<option value={60}
>{isMobile()
>{isMobile
? $LL.menu.settings.gameQuality.short.medium()
: $LL.menu.settings.gameQuality.long.medium()}</option
>
<option value={40}
>{isMobile()
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
>
<option value={20}
>{isMobile()
>{isMobile
? $LL.menu.settings.gameQuality.short.small()
: $LL.menu.settings.gameQuality.long.small()}</option
>
<option value={20}
>{isMobile
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
>
</select>
</div>
</section>
@ -120,25 +122,25 @@
<div class="nes-select is-dark">
<select bind:value={valueVideo}>
<option value={30}
>{isMobile()
>{isMobile
? $LL.menu.settings.videoQuality.short.high()
: $LL.menu.settings.videoQuality.long.high()}</option
>
<option value={20}
>{isMobile()
>{isMobile
? $LL.menu.settings.videoQuality.short.medium()
: $LL.menu.settings.videoQuality.long.medium()}</option
>
<option value={10}
>{isMobile()
? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option
>
<option value={5}
>{isMobile()
>{isMobile
? $LL.menu.settings.videoQuality.short.small()
: $LL.menu.settings.videoQuality.long.small()}</option
>
<option value={5}
>{isMobile
? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option
>
</select>
</div>
</section>
@ -199,6 +201,8 @@
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
div.settings-main {
height: calc(100% - 40px);
overflow-y: auto;
@ -236,7 +240,7 @@
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
@include media-breakpoint-up(md) {
div.settings-main {
section {
padding: 0;

View file

@ -32,6 +32,7 @@
max-width: 80vw;
overflow: auto;
text-align: center;
z-index: 500;
h2 {
font-family: "Press Start 2P";

View file

@ -75,6 +75,7 @@
max-width: 80vw;
overflow: auto;
text-align: center;
z-index: 450;
h2 {
font-family: "Press Start 2P";

View file

@ -2,7 +2,7 @@
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { localStreamStore, isSilentStore } from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy } from "svelte";
import { onDestroy, onMount } from "svelte";
import { srcObject } from "./Video/utils";
import LL from "../i18n/i18n-svelte";
@ -23,15 +23,75 @@
isSilent = value;
});
let cameraContainer: HTMLDivElement;
onMount(() => {
cameraContainer.addEventListener("transitionend", () => {
if (cameraContainer.classList.contains("hide")) {
cameraContainer.style.visibility = "hidden";
}
});
cameraContainer.addEventListener("transitionstart", () => {
if (!cameraContainer.classList.contains("hide")) {
cameraContainer.style.visibility = "visible";
}
});
});
onDestroy(unsubscribeIsSilent);
</script>
<div>
<div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline />
<SoundMeterWidget {stream} />
{/if}
</div>
<div class="is-silent" class:hide={isSilent}>{$LL.camera.my.silentZone()}</div>
<div
class="nes-container is-rounded my-cam-video-container"
class:hide={($localStreamStore.type !== "success" || !$obtainedMediaConstraintStore.video) && !isSilent}
bind:this={cameraContainer}
>
{#if isSilent}
<div class="is-silent">{$LL.camera.my.silentZone()}</div>
{:else if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="my-cam-video" use:srcObject={stream} autoplay muted playsinline />
<SoundMeterWidget {stream} />
{/if}
</div>
<style lang="scss">
@import "../../style/breakpoints.scss";
.my-cam-video-container {
position: absolute;
right: 15px;
bottom: 30px;
max-height: 20%;
transition: transform 1000ms;
padding: 0;
background-color: rgba(#000000, 0.6);
background-clip: content-box;
overflow: hidden;
line-height: 0;
z-index: 250;
&.nes-container.is-rounded {
border-image-outset: 1;
}
}
.my-cam-video-container.hide {
transform: translateX(200%);
}
.my-cam-video {
background-color: #00000099;
max-height: 20vh;
max-width: max(25vw, 150px);
width: 100%;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.is-silent {
font-size: 2em;
color: white;
padding: 40px 20px;
}
</style>

View file

@ -108,12 +108,16 @@
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
z-index: 650;
position: absolute;
height: 70vh;
width: 50vw;
top: 10vh;
margin: auto;
top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
section.report-menu-title {
display: grid;
@ -137,13 +141,4 @@
display: none;
}
}
@media only screen and (max-width: 800px) {
div.report-menu-main {
top: 21vh;
height: 60vh;
width: 100vw;
font-size: 0.5em;
}
}
</style>

View file

@ -47,6 +47,8 @@
</form>
<style lang="scss">
@import "../../../style/breakpoints.scss";
form.selectCompanionScene {
font-family: "Press Start 2P";
pointer-events: auto;
@ -85,7 +87,7 @@
}
}
@media only screen and (max-width: 800px) {
@include media-breakpoint-up(md) {
form.selectCompanionScene button.selectCharacterButtonLeft {
left: 5vw;
}

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { fly, fade } from "svelte/transition";
import { onMount } from "svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import LL from "../../i18n/i18n-svelte";
@ -13,6 +14,8 @@
onMount(() => {
timeToRead();
const gameScene = gameManager.getCurrentGameScene();
gameScene.playSound("audio-report-message");
});
function timeToRead() {
@ -53,18 +56,19 @@
on:click|preventDefault={closeBanMessage}>{nameButton}</button
>
</div>
<!-- svelte-ignore a11y-media-has-caption -->
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3" />
</audio>
</div>
<style lang="scss">
div.main-ban-message {
display: flex;
flex-direction: column;
position: relative;
top: 15vh;
position: absolute;
top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 850;
height: 70vh;
width: 60vw;

View file

@ -11,3 +11,9 @@
</div>
{/each}
</div>
<style lang="scss">
.main-ban-message-container {
z-index: 800;
}
</style>

View file

@ -42,14 +42,17 @@
div.main-text-message {
display: flex;
flex-direction: column;
position: absolute;
max-height: 25vh;
width: 80vw;
max-height: 25%;
width: 60%;
margin-right: auto;
margin-left: auto;
margin-bottom: 16px;
margin-top: 0;
top: 6%;
left: 0;
right: 0;
padding-bottom: 0;
z-index: 240;
pointer-events: auto;
background-color: #333333;

View file

@ -15,7 +15,8 @@
</div>
<style lang="scss">
div.main-text-message-container {
.main-text-message-container {
padding-top: 16px;
z-index: 800;
}
</style>

View file

@ -37,6 +37,7 @@
background-color: black;
border-radius: 30px 0 0 30px;
display: inline-flex;
z-index: 750;
img {
border-radius: 50%;

View file

@ -25,11 +25,17 @@
<style lang="scss">
div.error-div {
pointer-events: auto;
margin-top: 10vh;
margin-top: 4%;
margin-right: auto;
margin-left: auto;
left: 0;
right: 0;
position: absolute;
width: max-content;
max-width: 80vw;
z-index: 230;
height: auto !important;
background-clip: padding-box;
.button-bar {
text-align: center;

View file

@ -1,15 +1,33 @@
<script lang="typescript">
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import { srcObject } from "./utils";
export let clickable = false;
export let peer: ScreenSharingLocalMedia;
let stream = peer.stream;
export let cssClass: string | undefined;
let embedScreen: EmbedScreen;
if (stream) {
embedScreen = {
type: "streamable",
embed: stream as unknown as Streamable,
};
}
</script>
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
{#if stream}
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
<video
use:srcObject={stream}
autoplay
muted
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
{/if}
</div>

View file

@ -7,14 +7,110 @@
import type { Streamable } from "../../Stores/StreamableCollectionStore";
export let streamable: Streamable;
export let isHightlighted = false;
export let isClickable = false;
export let mozaicFullWidth = false;
export let mozaicQuarter = false;
</script>
<div class="media-container">
{#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable} />
{:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable} />
{:else}
<LocalStreamMediaBox peer={streamable} cssClass="" />
{/if}
<div
class="media-container nes-container is-rounded {isHightlighted ? 'hightlighted' : ''}"
class:clickable={isClickable}
class:mozaic-full-width={mozaicFullWidth}
class:mozaic-quarter={mozaicQuarter}
>
<div>
{#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable} clickable={isClickable} />
{:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable} clickable={isClickable} />
{:else}
<LocalStreamMediaBox peer={streamable} clickable={isClickable} cssClass="" />
{/if}
</div>
</div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.media-container {
display: flex;
margin-top: 4%;
margin-bottom: 4%;
margin-left: auto;
margin-right: auto;
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;
pointer-events: auto;
padding: 0;
max-height: 85%;
max-width: 85%;
&:hover {
margin-top: 2%;
margin-bottom: 2%;
}
&.hightlighted {
margin-top: 0% !important;
margin-bottom: 0% !important;
margin-left: 0% !important;
max-height: 100% !important;
max-width: 96% !important;
&:hover {
margin-top: 0% !important;
margin-bottom: 0% !important;
}
}
&.mozaic-full-width {
width: 95%;
max-width: 95%;
margin-left: 3%;
margin-right: 3%;
margin-top: auto;
margin-bottom: auto;
&:hover {
margin-top: auto;
margin-bottom: auto;
}
}
&.mozaic-quarter {
width: 95%;
max-width: 95%;
margin-top: auto;
margin-bottom: auto;
&:hover {
margin-top: auto;
margin-bottom: auto;
}
}
&.nes-container.is-rounded {
border-image-outset: 1;
}
&.clickable {
cursor: url("../../../style/images/cursor_pointer.png"), pointer;
}
> div {
background-color: rgba(0, 0, 0, 0.6);
display: flex;
width: 100%;
}
}
@include media-breakpoint-only(md) {
.media-container {
margin-top: 10%;
margin-bottom: 10%;
}
}
</style>

View file

@ -1,26 +0,0 @@
<script lang="ts">
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { afterUpdate } from "svelte";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte";
afterUpdate(() => {
biggestAvailableAreaStore.recompute();
});
</script>
<div class="main-section">
{#if $videoFocusStore}
{#key $videoFocusStore.uniqueId}
<MediaBox streamable={$videoFocusStore} />
{/key}
{/if}
</div>
<aside class="sidebar">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if peer !== $videoFocusStore}
<MediaBox streamable={peer} />
{/if}
{/each}
</aside>

View file

@ -1,12 +1,26 @@
<script lang="ts">
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { getColorByString, srcObject } from "./utils";
export let clickable = false;
export let peer: ScreenSharingPeer;
let streamStore = peer.streamStore;
let name = peer.userName;
let statusStore = peer.statusStore;
let embedScreen: EmbedScreen;
if (peer) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
</script>
<div class="video-container">
@ -16,11 +30,17 @@
{#if $statusStore === "error"}
<div class="rtc-error" />
{/if}
{#if $streamStore === null}
<i style="background-color: {getColorByString(name)};">{name}</i>
{:else}
{#if $streamStore !== null}
<!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
<i class="container">
<span style="background-color: {getColorByString(name)};">{name}</span>
</i>
<video
use:srcObject={$streamStore}
autoplay
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
{/if}
</div>
@ -29,5 +49,10 @@
video {
width: 100%;
}
i {
span {
padding: 2px 32px;
}
}
}
</style>

View file

@ -4,11 +4,17 @@
import microphoneCloseImg from "../images/microphone-close.svg";
import reportImg from "./images/report.svg";
import blockSignImg from "./images/blockSign.svg";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { getColorByString, srcObject } from "./utils";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import Woka from "../Woka/Woka.svelte";
import { onMount } from "svelte";
import { isMediaBreakpointOnly } from "../../Utils/BreakpointsUtils";
export let clickable = false;
export let peer: VideoPeer;
let streamStore = peer.streamStore;
@ -19,9 +25,32 @@
function openReport(peer: VideoPeer): void {
showReportScreenStore.set({ userId: peer.userId, userName: peer.userName });
}
let embedScreen: EmbedScreen;
let videoContainer: HTMLDivElement;
let minimized = isMediaBreakpointOnly("md");
if (peer) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
function noDrag() {
return false;
}
const resizeObserver = new ResizeObserver(() => {
minimized = isMediaBreakpointOnly("md");
});
onMount(() => {
resizeObserver.observe(videoContainer);
});
</script>
<div class="video-container">
<div class="video-container" class:no-clikable={!clickable} bind:this={videoContainer}>
{#if $statusStore === "connecting"}
<div class="connecting-spinner" />
{/if}
@ -29,43 +58,46 @@
<div class="rtc-error" />
{/if}
<!-- {#if !$constraintStore || $constraintStore.video === false} -->
<i
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}"
style="background-color: {getColorByString(name)};"
>
<span>{peer.userName}</span>
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
<i class="container">
<span style="background-color: {getColorByString(name)};">{name}</span>
</i>
<div class="woka-icon {($constraintStore && $constraintStore.video !== false) || minimized ? '' : 'no-video'}">
<Woka userId={peer.userId} placeholderSrc={""} />
</div>
<!-- {/if} -->
{#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} class="active" alt="Muted" />
<img
src={microphoneCloseImg}
class="active noselect"
draggable="false"
on:dragstart|preventDefault={noDrag}
alt="Muted"
/>
{/if}
<button class="report" on:click={() => openReport(peer)}>
<img alt="Report this user" src={reportImg} />
<span>Report/Block</span>
<img alt="Report this user" draggable="false" on:dragstart|preventDefault={noDrag} src={reportImg} />
<span class="noselect">Report/Block</span>
</button>
<!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
<img src={blockSignImg} class="block-logo" alt="Block" />
<video
class:no-video={!$constraintStore || $constraintStore.video === false}
use:srcObject={$streamStore}
autoplay
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
<img src={blockSignImg} draggable="false" on:dragstart|preventDefault={noDrag} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore} />
{/if}
</div>
<style>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
padding-top: 15px;
}
.minimized {
left: auto;
transform: scale(0.5);
opacity: 0.5;
}
.woka-icon {
margin-right: 3px;
video.no-video {
visibility: collapse;
}
</style>

View file

@ -1,16 +1,16 @@
<script lang="ts">
import { LayoutMode } from "../../WebRtc/LayoutManager";
import { layoutModeStore } from "../../Stores/StreamableCollectionStore";
import PresentationLayout from "./PresentationLayout.svelte";
import ChatLayout from "./ChatLayout.svelte";
// import {LayoutMode} from "../../WebRtc/LayoutManager";
// import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
// import PresentationLayout from "./PresentationLayout.svelte";
// import ChatLayout from "./ChatLayout.svelte";
</script>
<div class="video-overlay">
{#if $layoutModeStore === LayoutMode.Presentation}
<!-- {#if $layoutModeStore === LayoutMode.Presentation }
<PresentationLayout />
{:else}
<ChatLayout />
{/if}
{/if} -->
</div>
<style lang="scss">

View file

@ -57,6 +57,7 @@
height: 120px;
margin: auto;
animation: spin 2s linear infinite;
z-index: 350;
}
@keyframes spin {

View file

@ -27,18 +27,28 @@
<style lang="scss">
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
width: 80%;
background-color: #f9e81e;
color: #14304c;
text-align: center;
position: absolute;
left: 50%;
top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
transform: translate(-50%, 0);
font-family: Lato;
min-width: 300px;
opacity: 0.9;
z-index: 2;
z-index: 700;
h2 {
padding: 5px;
}
a {
color: #ff475a;
}
}
</style>

View file

@ -22,10 +22,21 @@
src = source ?? placeholderSrc;
});
function noDrag() {
return false;
}
onDestroy(unsubscribe);
</script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
<img
{src}
alt=""
class="nes-pointer noselect"
style="--theme-width: {width}; --theme-height: {height}"
draggable="false"
on:dragstart|preventDefault={noDrag}
/>
<style>
img {

View file

@ -49,6 +49,8 @@
</form>
<style lang="scss">
@import "../../../style/breakpoints.scss";
form.selectCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
@ -91,7 +93,7 @@
}
}
@media only screen and (max-width: 800px) {
@include media-breakpoint-up(md) {
form.selectCharacterScene button.selectCharacterButtonLeft {
left: 5vw;
}

View file

@ -183,14 +183,14 @@ class ConnectionManager {
window.location.hash;
}
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value
await localUserStore.setLastRoomUrl(new URL(roomPath).href);
//get detail map for anonymous login and set texture in local storage
//before set token of user we must load room and all information. For example the mandatory authentication could be require on current room
this._currentRoom = await Room.createRoom(new URL(roomPath));
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value
await localUserStore.setLastRoomUrl(this._currentRoom.href);
//todo: add here some kind of warning if authToken has expired.
if (!this.authToken && !this._currentRoom.authenticationMandatory) {
await this.anonymousLogin();

View file

@ -1,33 +1,46 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const START_ROOM_URL: string =
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const ICON_URL = process.env.ICON_URL || "//icon.workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
const TURN_USER: string = process.env.TURN_USER || "";
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || "";
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";
declare global {
interface Window {
env?: Record<string, string>;
}
}
const getEnv = (key: string): string | undefined => {
if (global.window?.env) {
return global.window.env[key];
}
if (global.process?.env) {
return global.process.env[key];
}
return;
};
const DEBUG_MODE: boolean = getEnv("DEBUG_MODE") == "true";
const START_ROOM_URL: string = getEnv("START_ROOM_URL") || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const PUSHER_URL = getEnv("PUSHER_URL") || "//pusher.workadventure.localhost";
export const ADMIN_URL = getEnv("ADMIN_URL") || "//workadventu.re";
const UPLOADER_URL = getEnv("UPLOADER_URL") || "//uploader.workadventure.localhost";
const ICON_URL = getEnv("ICON_URL") || "//icon.workadventure.localhost";
const STUN_SERVER: string = getEnv("STUN_SERVER") || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = getEnv("TURN_SERVER") || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = getEnv("SKIP_RENDER_OPTIMIZATIONS") == "true";
const DISABLE_NOTIFICATIONS: boolean = getEnv("DISABLE_NOTIFICATIONS") == "true";
const TURN_USER: string = getEnv("TURN_USER") || "";
const TURN_PASSWORD: string = getEnv("TURN_PASSWORD") || "";
const JITSI_URL: string | undefined = getEnv("JITSI_URL") === "" ? undefined : getEnv("JITSI_URL");
const JITSI_PRIVATE_MODE: boolean = getEnv("JITSI_PRIVATE_MODE") == "true";
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
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const NODE_ENV = process.env.NODE_ENV || "development";
export const CONTACT_URL = process.env.CONTACT_URL || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined;
export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || "";
export const POSTHOG_URL = process.env.POSTHOG_URL || undefined;
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER;
const FALLBACK_LOCALE = process.env.FALLBACK_LOCALE || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export const MAX_USERNAME_LENGTH = parseInt(getEnv("MAX_USERNAME_LENGTH") || "") || 8;
export const MAX_PER_GROUP = parseInt(getEnv("MAX_PER_GROUP") || "4");
export const DISPLAY_TERMS_OF_USE = getEnv("DISPLAY_TERMS_OF_USE") == "true";
export const NODE_ENV = getEnv("NODE_ENV") || "development";
export const CONTACT_URL = getEnv("CONTACT_URL") || undefined;
export const PROFILE_URL = getEnv("PROFILE_URL") || undefined;
export const POSTHOG_API_KEY: string = (getEnv("POSTHOG_API_KEY") as string) || "";
export const POSTHOG_URL = getEnv("POSTHOG_URL") || undefined;
export const DISABLE_ANONYMOUS: boolean = getEnv("DISABLE_ANONYMOUS") === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = getEnv("OPID_LOGIN_SCREEN_PROVIDER");
const FALLBACK_LOCALE = getEnv("FALLBACK_LOCALE") || undefined;
export {
DEBUG_MODE,

View file

@ -63,8 +63,6 @@ export class SoundMeter {
//this.slow = 0.95 * that.slow + 0.05 * that.instant;
//this.clip = clipcount / input.length;
//console.log('instant', this.instant, 'clip', this.clip);
return this.instant;
}

View file

@ -1,4 +1,10 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapObject, ITiledMapProperty } from "../Map/ITiledMap";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapProperty,
ITiledMapTileLayer,
} from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
@ -291,6 +297,31 @@ export class GameMap {
}
}
public getRandomPositionFromLayer(layerName: string): { x: number; y: number } {
const layer = this.findLayer(layerName) as ITiledMapTileLayer;
if (!layer) {
throw new Error(`No layer "${layerName}" was found`);
}
const tiles = layer.data;
if (!tiles) {
throw new Error(`No tiles in "${layerName}" were found`);
}
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possiblePositions: { x: number; y: number }[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
possiblePositions.push({ x: key % layer.width, y: Math.floor(key / layer.width) });
});
if (possiblePositions.length > 0) {
return MathUtils.randomFromArray(possiblePositions);
}
throw new Error("No possible position found");
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
}

View file

@ -20,7 +20,6 @@ export enum GameMapProperties {
OPEN_WEBSITE = "openWebsite",
OPEN_WEBSITE_ALLOW_API = "openWebsiteAllowApi",
OPEN_WEBSITE_POLICY = "openWebsitePolicy",
OPEN_WEBSITE_WIDTH = "openWebsiteWidth",
OPEN_WEBSITE_POSITION = "openWebsitePosition",
OPEN_WEBSITE_TRIGGER = "openWebsiteTrigger",
OPEN_WEBSITE_TRIGGER_MESSAGE = "openWebsiteTriggerMessage",

View file

@ -9,21 +9,21 @@ import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
enum OpenCoWebsiteState {
LOADING,
ASLEEP,
OPENED,
MUST_BE_CLOSE,
}
interface OpenCoWebsite {
coWebsite: CoWebsite | undefined;
coWebsite: CoWebsite;
state: OpenCoWebsiteState;
}
export class GameMapPropertiesListener {
private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>();
private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>();
constructor(private scene: GameScene, private gameMap: GameMap) {}
@ -64,10 +64,8 @@ export class GameMapPropertiesListener {
let openWebsiteProperty: string | undefined;
let allowApiProperty: boolean | undefined;
let websitePolicyProperty: string | undefined;
let websiteWidthProperty: number | undefined;
let websitePositionProperty: number | undefined;
let websiteTriggerProperty: string | undefined;
let websiteTriggerMessageProperty: string | undefined;
layer.properties.forEach((property) => {
switch (property.name) {
@ -80,18 +78,12 @@ export class GameMapPropertiesListener {
case GameMapProperties.OPEN_WEBSITE_POLICY:
websitePolicyProperty = property.value as string | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_WIDTH:
websiteWidthProperty = property.value as number | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_POSITION:
websitePositionProperty = property.value as number | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_TRIGGER:
websiteTriggerProperty = property.value as string | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE:
websiteTriggerMessageProperty = property.value as string | undefined;
break;
}
});
@ -105,27 +97,30 @@ export class GameMapPropertiesListener {
return;
}
const coWebsite = coWebsiteManager.addCoWebsite(
openWebsiteProperty,
this.scene.MapUrlFile,
allowApiProperty,
websitePolicyProperty,
websitePositionProperty,
false
);
this.coWebsitesOpenByLayer.set(layer, {
coWebsite: undefined,
state: OpenCoWebsiteState.LOADING,
coWebsite: coWebsite,
state: OpenCoWebsiteState.ASLEEP,
});
const openWebsiteFunction = () => {
coWebsiteManager
.loadCoWebsite(
openWebsiteProperty as string,
this.scene.MapUrlFile,
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
websitePositionProperty
)
.loadCoWebsite(coWebsite)
.then((coWebsite) => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e));
coWebsiteManager.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during a co-website closing");
});
this.coWebsitesOpenByLayer.delete(layer);
this.coWebsitesActionTriggerByLayer.delete(layer);
} else {
this.coWebsitesOpenByLayer.set(layer, {
coWebsite,
@ -133,27 +128,17 @@ export class GameMapPropertiesListener {
});
}
})
.catch((e) => console.error(e));
.catch(() => {
console.error("Error during loading a co-website: " + coWebsite.url);
});
layoutManagerActionStore.removeAction(actionUuid);
};
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) {
if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site";
}
this.coWebsitesActionTriggerByLayer.set(layer, actionUuid);
layoutManagerActionStore.addAction({
uuid: actionUuid,
type: "message",
message: websiteTriggerMessageProperty,
callback: () => openWebsiteFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
if (
!localUserStore.getForceCowebsiteTrigger() &&
websiteTriggerProperty !== ON_ACTION_TRIGGER_BUTTON
) {
openWebsiteFunction();
}
});
@ -194,7 +179,7 @@ export class GameMapPropertiesListener {
return;
}
if (coWebsiteOpen.state === OpenCoWebsiteState.LOADING) {
if (coWebsiteOpen.state === OpenCoWebsiteState.ASLEEP) {
coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE;
}
@ -203,26 +188,6 @@ export class GameMapPropertiesListener {
}
this.coWebsitesOpenByLayer.delete(layer);
if (!websiteTriggerProperty) {
return;
}
const actionStore = get(layoutManagerActionStore);
const actionTriggerUuid = this.coWebsitesActionTriggerByLayer.get(layer);
if (!actionTriggerUuid) {
return;
}
const action =
actionStore && actionStore.length > 0
? actionStore.find((action) => action.uuid === actionTriggerUuid)
: undefined;
if (action) {
layoutManagerActionStore.removeAction(actionTriggerUuid);
}
});
};

View file

@ -250,7 +250,7 @@ export class GameScene extends DirtyScene {
}
this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3");
this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3");
//this.load.audio('audio-report-message', '/resources/objects/report-message.mp3');
this.load.audio("audio-report-message", "/resources/objects/report-message.mp3");
this.sound.pauseOnBlur = false;
this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => {
@ -558,6 +558,12 @@ export class GameScene extends DirtyScene {
.catch((e) => console.error(e));
}
this.pathfindingManager = new PathfindingManager(
this,
this.gameMap.getCollisionsGrid(),
this.gameMap.getTileDimensions()
);
//notify game manager can to create currentUser in map
this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
@ -632,13 +638,9 @@ export class GameScene extends DirtyScene {
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
const newPeerNumber = peers.size;
if (newPeerNumber > oldPeerNumber) {
this.sound.play("audio-webrtc-in", {
volume: 0.2,
});
this.playSound("audio-webrtc-in");
} else if (newPeerNumber < oldPeerNumber) {
this.sound.play("audio-webrtc-out", {
volume: 0.2,
});
this.playSound("audio-webrtc-out");
}
oldPeerNumber = newPeerNumber;
});
@ -1259,21 +1261,21 @@ ${escapedMessage}
throw new Error("Unknown query source");
}
const coWebsite = await coWebsiteManager.loadCoWebsite(
const coWebsite = coWebsiteManager.addCoWebsite(
openCoWebsite.url,
iframeListener.getBaseUrlFromSource(source),
openCoWebsite.allowApi,
openCoWebsite.allowPolicy,
openCoWebsite.position
openCoWebsite.position,
openCoWebsite.closable ?? true
);
if (!coWebsite) {
throw new Error("Error on opening co-website");
if (openCoWebsite.lazy !== undefined && !openCoWebsite.lazy) {
await coWebsiteManager.loadCoWebsite(coWebsite);
}
return {
id: coWebsite.iframe.id,
position: coWebsite.position,
};
});
@ -1283,7 +1285,6 @@ ${escapedMessage}
return coWebsites.map((coWebsite: CoWebsite) => {
return {
id: coWebsite.iframe.id,
position: coWebsite.position,
};
});
});
@ -1559,6 +1560,12 @@ ${escapedMessage}
}
}
public playSound(sound: string) {
this.sound.play(sound, {
volume: 0.2,
});
}
public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsites().catch((e) => console.error(e));
@ -1596,7 +1603,10 @@ ${escapedMessage}
this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close();
mediaManager.hideGameOverlay();
//When we leave game, the camera is stop to be reopen after.
// I think that we could keep camera status and the scene can manage camera setup
//TODO find wy chrome don't manage correctly a multiple ask mediaDevices
//mediaManager.hideMyCamera();
for (const iframeEvents of this.iframeSubscriptionList) {
iframeEvents.unsubscribe();
@ -1729,6 +1739,22 @@ ${escapedMessage}
this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey);
});
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) {
if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
@ -2070,7 +2096,18 @@ ${escapedMessage}
biggestAvailableAreaStore.recompute();
}
private startJitsi(roomName: string, jwt?: string): void {
public enableMediaBehaviors() {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT);
this.connection?.setSilent(!!silent);
mediaManager.showMyCamera();
}
public disableMediaBehaviors() {
this.connection?.setSilent(true);
mediaManager.hideMyCamera();
}
public startJitsi(roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
@ -2081,28 +2118,21 @@ ${escapedMessage}
GameMapProperties.JITSI_INTERFACE_CONFIG
);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined;
jitsiFactory
.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth)
.catch((e) => console.error(e));
this.connection?.setSilent(true);
mediaManager.hideGameOverlay();
analyticsClient.enteredJitsi(roomName, this.room.id);
//permit to stop jitsi when user close iframe
mediaManager.addTriggerCloseJitsiFrameButton("close-jitsi", () => {
this.stopJitsi();
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl).catch(() => {
console.error("Cannot start a Jitsi co-website");
});
this.disableMediaBehaviors();
analyticsClient.enteredJitsi(roomName, this.room.id);
}
private stopJitsi(): void {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT);
this.connection?.setSilent(!!silent);
jitsiFactory.stop();
mediaManager.showGameOverlay();
mediaManager.removeTriggerCloseJitsiFrameButton("close-jitsi");
public stopJitsi(): void {
const coWebsite = coWebsiteManager.searchJitsi();
if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => {
console.error("Error during Jitsi co-website closing", e);
});
}
}
//todo: put this into an 'orchestrator' scene (EntryScene?)

View file

@ -41,7 +41,7 @@ export class PlayerMovement {
oldX: this.startPosition.x,
oldY: this.startPosition.y,
direction: this.endPosition.direction,
moving: this.endPosition.moving,
moving: this.isOutdated(tick) ? false : this.endPosition.moving,
};
}
}

View file

@ -11,10 +11,10 @@ import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import { SelectCharacterSceneName } from "./SelectCharacterScene";
import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
import { get } from "svelte/store";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
export const CustomizeSceneName = "CustomizeScene";
@ -67,12 +67,12 @@ export class CustomizeScene extends AbstractCharacterScene {
customCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1;
customCharacterSceneVisibleStore.set(true);
});
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1;
this.Rectangle = this.add.rectangle(
this.cameras.main.worldView.x + this.cameras.main.width / 2,

View file

@ -8,8 +8,6 @@ import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store";
import { localeDetector } from "../../i18n/locales";
const $LL = get(LL);
export const EntrySceneName = "EntryScene";
/**
@ -43,6 +41,7 @@ export class EntryScene extends Scene {
this.scene.start(nextSceneName);
})
.catch((err) => {
const $LL = get(LL);
if (err.response && err.response.status == 404) {
ErrorScene.showError(
new WAError(

View file

@ -12,8 +12,8 @@ import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
@ -60,7 +60,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
selectCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
selectCharacterSceneVisibleStore.set(true);
});
@ -69,7 +69,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);

View file

@ -9,7 +9,7 @@ import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
export const SelectCompanionSceneName = "SelectCompanionScene";
@ -44,7 +44,7 @@ export class SelectCompanionScene extends ResizableScene {
selectCompanionSceneVisibleStore.set(true);
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);

View file

@ -37,19 +37,17 @@ export class WaScaleManager {
height: height * devicePixelRatio,
});
if (gameSize.width == 0) {
return;
if (realSize.width !== 0 && gameSize.width !== 0 && devicePixelRatio !== 0) {
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
}
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.setZoom(this.actualZoom);
this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + "px";
style.height = Math.ceil(realSize.height / devicePixelRatio) + "px";
style.width = Math.ceil(realSize.width !== 0 ? realSize.width / devicePixelRatio : 0) + "px";
style.height = Math.ceil(realSize.height !== 0 ? realSize.height / devicePixelRatio : 0) + "px";
// Resize the game element at the same size at the canvas
const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game").style;

View file

@ -2,14 +2,14 @@ import { get, writable } from "svelte/store";
import type { Box } from "../WebRtc/LayoutManager";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { layoutModeStore } from "./StreamableCollectionStore";
import { embedScreenLayout } from "./EmbedScreensStore";
/**
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/
function findBiggestAvailableArea(): Box {
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
if (get(layoutModeStore) === LayoutMode.VideoChat) {
if (get(embedScreenLayout) === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
const htmlChildren = Array.from(children.values());

View file

@ -0,0 +1,51 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
function createCoWebsiteStore() {
const { subscribe, set, update } = writable(Array<CoWebsite>());
set(Array<CoWebsite>());
return {
subscribe,
add: (coWebsite: CoWebsite, position?: number) => {
coWebsite.state.subscribe((value) => {
update((currentArray) => currentArray);
});
if (position || position === 0) {
update((currentArray) => {
if (position === 0) {
return [coWebsite, ...currentArray];
} else if (currentArray.length > position) {
const test = [...currentArray.splice(position, 0, coWebsite)];
return [...currentArray.splice(position, 0, coWebsite)];
}
return [...currentArray, coWebsite];
});
return;
}
update((currentArray) => [...currentArray, coWebsite]);
},
remove: (coWebsite: CoWebsite) => {
update((currentArray) => [
...currentArray.filter((currentCoWebsite) => currentCoWebsite.iframe.id !== coWebsite.iframe.id),
]);
},
empty: () => {
set(Array<CoWebsite>());
},
};
}
export const coWebsites = createCoWebsiteStore();
export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.filter((coWebsite) => get(coWebsite.state) !== "asleep")
);
export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => get(coWebsite.state) !== "asleep")
);

View file

@ -0,0 +1,51 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { coWebsites } from "./CoWebsiteStore";
import { Streamable, streamableCollectionStore } from "./StreamableCollectionStore";
export type EmbedScreen =
| {
type: "streamable";
embed: Streamable;
}
| {
type: "cowebsite";
embed: CoWebsite;
};
function createHighlightedEmbedScreenStore() {
const { subscribe, set, update } = writable<EmbedScreen | null>(null);
return {
subscribe,
highlight: (embedScreen: EmbedScreen) => {
set(embedScreen);
},
removeHighlight: () => {
set(null);
},
toggleHighlight: (embedScreen: EmbedScreen) => {
update((currentEmbedScreen) =>
!currentEmbedScreen ||
embedScreen.type !== currentEmbedScreen.type ||
(embedScreen.type === "cowebsite" &&
currentEmbedScreen.type === "cowebsite" &&
embedScreen.embed.iframe.id !== currentEmbedScreen.embed.iframe.id) ||
(embedScreen.type === "streamable" &&
currentEmbedScreen.type === "streamable" &&
embedScreen.embed.uniqueId !== currentEmbedScreen.embed.uniqueId)
? embedScreen
: null
);
},
};
}
export const highlightedEmbedScreen = createHighlightedEmbedScreenStore();
export const embedScreenLayout = writable<LayoutMode>(LayoutMode.Presentation);
export const hasEmbedScreen = derived(
[streamableCollectionStore],
($values) => get(streamableCollectionStore).size + get(coWebsites).length > 0
);

View file

@ -1,17 +0,0 @@
import { writable } from "svelte/store";
/**
* A store that contains whether the game overlay is shown or not.
* Typically, the overlay is hidden when entering Jitsi meet.
*/
function createGameOverlayVisibilityStore() {
const { subscribe, set, update } = writable(false);
return {
subscribe,
showGameOverlay: () => set(true),
hideGameOverlay: () => set(false),
};
}
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();

View file

@ -51,6 +51,6 @@ function createLayoutManagerAction() {
export const layoutManagerActionStore = createLayoutManagerAction();
export const layoutManagerVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => {
export const layoutManagerActionVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => {
return !!$layoutManagerActionStore.length;
});

View file

@ -6,7 +6,7 @@ import { BrowserTooOldError } from "./Errors/BrowserTooOldError";
import { errorStore } from "./ErrorStore";
import { getNavigatorType, isIOS, NavigatorType } from "../WebRtc/DeviceUtils";
import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import { myCameraVisibilityStore } from "./MyCameraStoreVisibility";
import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
@ -233,7 +233,7 @@ export const mediaStreamConstraintsStore = derived(
[
requestedCameraState,
requestedMicrophoneState,
gameOverlayVisibilityStore,
myCameraVisibilityStore,
enableCameraSceneVisibilityStore,
videoConstraintStore,
audioConstraintStore,
@ -245,7 +245,7 @@ export const mediaStreamConstraintsStore = derived(
[
$requestedCameraState,
$requestedMicrophoneState,
$gameOverlayVisibilityStore,
$myCameraVisibilityStore,
$enableCameraSceneVisibilityStore,
$videoConstraintStore,
$audioConstraintStore,
@ -283,7 +283,7 @@ export const mediaStreamConstraintsStore = derived(
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
if ($myCameraVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}

View file

@ -0,0 +1,8 @@
import { writable } from "svelte/store";
/**
* A store that contains whether my camera & actions is shown or not.
* Typically, the overlay is hidden when entering Jitsi meet.
*/
export const myCameraVisibilityStore = writable(false);

View file

@ -1,7 +1,7 @@
import { derived, Readable, readable, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
import type { LocalStreamStoreValue } from "./MediaStore";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import { myCameraVisibilityStore } from "./MyCameraStoreVisibility";
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -41,8 +41,8 @@ let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
* A store containing the media constraints we want to apply.
*/
export const screenSharingConstraintsStore = derived(
[requestedScreenSharingState, gameOverlayVisibilityStore, peerStore],
([$requestedScreenSharingState, $gameOverlayVisibilityStore, $peerStore], set) => {
[requestedScreenSharingState, myCameraVisibilityStore, peerStore],
([$requestedScreenSharingState, $myCameraVisibilityStore, $peerStore], set) => {
let currentVideoConstraint: boolean | MediaTrackConstraints = true;
let currentAudioConstraint: boolean | MediaTrackConstraints = false;
@ -53,7 +53,7 @@ export const screenSharingConstraintsStore = derived(
}
// Disable screen sharing when in a Jitsi
if (!$gameOverlayVisibilityStore) {
if (!$myCameraVisibilityStore) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}

View file

@ -1,13 +1,10 @@
import { derived, get, Readable, writable } from "svelte/store";
import { derived, get, Readable } from "svelte/store";
import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore";
import { peerStore, screenSharingStreamStore } from "./PeerStore";
import type { RemotePeer } from "../WebRtc/SimplePeer";
import { LayoutMode } from "../WebRtc/LayoutManager";
export type Streamable = RemotePeer | ScreenSharingLocalMedia;
export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
/**
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
*/

View file

@ -96,6 +96,7 @@ export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
const unsubscribe = storeByKey.subscribe((newMapValue) => {
if (unsubscribeDeepStore) {
unsubscribeDeepStore();
unsubscribeDeepStore = undefined;
}
if (newMapValue === undefined) {
set(undefined);
@ -115,6 +116,7 @@ export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
unsubscribe();
if (unsubscribeDeepStore) {
unsubscribeDeepStore();
unsubscribeDeepStore = undefined;
}
};
});

View file

@ -45,8 +45,28 @@ class UrlManager {
}
public getStartLayerNameFromUrl(): string | null {
const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null;
const parameters = this.getHashParameters();
for (const key in parameters) {
if (parameters[key] === undefined) {
return key;
}
}
return null;
}
public getHashParameter(name: string): string | undefined {
return this.getHashParameters()[name];
}
private getHashParameters(): Record<string, string> {
return window.location.hash
.substring(1)
.split("&")
.reduce((res: Record<string, string>, item: string) => {
const parts = item.split("=");
res[parts[0]] = parts[1];
return res;
}, {});
}
pushStartLayerNameToUrl(startLayerName: string): void {

View file

@ -0,0 +1,112 @@
type InternalBreakpoint = {
beforeBreakpoint: InternalBreakpoint | undefined;
nextBreakpoint: InternalBreakpoint | undefined;
pixels: number;
};
function generateBreakpointsMap(): Map<string, InternalBreakpoint> {
// If is changed don't forget to also change it on SASS.
const breakpoints: { [key: string]: number } = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400,
};
let beforeBreakpoint: InternalBreakpoint | undefined;
let beforeBreakpointTag: string | undefined;
const mapRender = new Map<string, InternalBreakpoint>();
for (const breakpoint in breakpoints) {
const newBreakpoint = {
beforeBreakpoint: beforeBreakpoint,
nextBreakpoint: undefined,
pixels: breakpoints[breakpoint],
};
if (beforeBreakpointTag && beforeBreakpoint) {
beforeBreakpoint.nextBreakpoint = newBreakpoint;
mapRender.set(beforeBreakpointTag, beforeBreakpoint);
}
mapRender.set(breakpoint, {
beforeBreakpoint: beforeBreakpoint,
nextBreakpoint: undefined,
pixels: breakpoints[breakpoint],
});
beforeBreakpointTag = breakpoint;
beforeBreakpoint = newBreakpoint;
}
return mapRender;
}
const breakpoints = generateBreakpointsMap();
export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
export function isMediaBreakpointUp(breakpoint: Breakpoint): boolean {
if (breakpoint === "xxl") {
return true;
}
const breakpointObject = breakpoints.get(breakpoint);
if (!breakpointObject) {
throw new Error(`Unknown breakpoint: ${breakpoint}`);
}
if (!breakpointObject.nextBreakpoint) {
return false;
}
return breakpointObject.nextBreakpoint.pixels - 1 >= window.innerWidth;
}
export function isMediaBreakpointDown(breakpoint: Breakpoint): boolean {
if (breakpoint === "xs") {
return true;
}
const breakpointObject = breakpoints.get(breakpoint);
if (!breakpointObject) {
throw new Error(`Unknown breakpoint: ${breakpoint}`);
}
return breakpointObject.pixels <= window.innerWidth;
}
export function isMediaBreakpointOnly(breakpoint: Breakpoint): boolean {
const breakpointObject = breakpoints.get(breakpoint);
if (!breakpointObject) {
throw new Error(`Unknown breakpoint: ${breakpoint}`);
}
return (
breakpointObject.pixels <= window.innerWidth &&
(!breakpointObject.nextBreakpoint || breakpointObject.nextBreakpoint.pixels - 1 >= window.innerWidth)
);
}
export function isMediaBreakpointBetween(startBreakpoint: Breakpoint, endBreakpoint: Breakpoint): boolean {
const startBreakpointObject = breakpoints.get(startBreakpoint);
const endBreakpointObject = breakpoints.get(endBreakpoint);
if (!startBreakpointObject) {
throw new Error(`Unknown start breakpoint: ${startBreakpointObject}`);
}
if (!endBreakpointObject) {
throw new Error(`Unknown end breakpoint: ${endBreakpointObject}`);
}
return (
startBreakpointObject.pixels <= innerWidth &&
(!endBreakpointObject.nextBreakpoint || endBreakpointObject.nextBreakpoint.pixels - 1 >= window.innerWidth)
);
}

View file

@ -31,4 +31,8 @@ export class MathUtils {
const distance = Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
return squared ? Math.sqrt(distance) : distance;
}
public static randomFromArray<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,11 @@ export class HtmlUtils {
throw new Error("Cannot find HTML element with id '" + id + "'");
}
public static getElementById<T extends HTMLElement>(id: string): T | undefined {
const elem = document.getElementById(id);
return HtmlUtils.isHtmlElement<T>(elem) ? elem : undefined;
}
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
const elem = document.querySelector<T>(selector);
if (HtmlUtils.isHtmlElement<T>(elem)) {
@ -35,6 +40,7 @@ export class HtmlUtils {
const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => {
url = HtmlUtils.htmlDecode(url);
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
@ -45,6 +51,15 @@ export class HtmlUtils {
});
}
private static htmlDecode(input: string): string {
const doc = new DOMParser().parseFromString(input, "text/html");
const text = doc.documentElement.textContent;
if (text === null) {
throw new Error("Unexpected non parseable string");
}
return text;
}
public static isClickedInside(event: MouseEvent, target: HTMLElement): boolean {
return !!event.composedPath().find((et) => et === target);
}

View file

@ -7,6 +7,7 @@ interface jitsiConfigInterface {
startWithAudioMuted: boolean;
startWithVideoMuted: boolean;
prejoinPageEnabled: boolean;
disableDeepLinking: boolean;
}
interface JitsiOptions {
@ -40,6 +41,7 @@ const getDefaultConfig = (): jitsiConfigInterface => {
startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false,
disableDeepLinking: false,
};
};
@ -132,64 +134,112 @@ class JitsiFactory {
return slugify(instance.replace("/", "-") + "-" + roomName);
}
public start(
public async start(
roomName: string,
playerName: string,
jwt?: string,
config?: object,
interfaceConfig?: object,
jitsiUrl?: string,
jitsiWidth?: number
): Promise<CoWebsite> {
return coWebsiteManager.addCoWebsite(
async (cowebsiteDiv) => {
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
jitsiUrl?: string
) {
const coWebsite = coWebsiteManager.searchJitsi();
const domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
await this.loadJitsiScript(domain);
if (coWebsite) {
await coWebsiteManager.closeCoWebsite(coWebsite);
}
const options: JitsiOptions = {
roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: cowebsiteDiv,
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!options.jwt) {
delete options.jwt;
}
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
return new Promise((resolve, reject) => {
const doResolve = (): void => {
const iframe = cowebsiteDiv.querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe === null) {
throw new Error("Could not find Jitsi Iframe");
}
resolve(iframe);
};
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => doResolve(), 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);
const domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
await this.loadJitsiScript(domain);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
const options: JitsiOptions = {
roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: coWebsiteManager.getCoWebsiteBuffer(),
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!options.jwt) {
delete options.jwt;
}
const doResolve = (): void => {
const iframe = coWebsiteManager.getCoWebsiteBuffer().querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe && this.jitsiApi) {
const coWebsite = coWebsiteManager.addCoWebsiteFromIframe(iframe, false, undefined, 0, false, true);
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(coWebsite);
});
},
jitsiWidth,
0
);
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(coWebsite);
});
}
coWebsiteManager.resizeAllIframes();
};
this.jitsiApi = undefined;
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => doResolve(), 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);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
}
private closeOrUnload = function (coWebsite: CoWebsite) {
if (coWebsite.closable) {
coWebsiteManager.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during closing a Jitsi Meet");
});
} else {
coWebsiteManager.unloadCoWebsite(coWebsite).catch(() => {
console.error("Error during unloading a Jitsi Meet");
});
}
};
public restart() {
if (!this.jitsiApi) {
return;
}
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
const coWebsite = coWebsiteManager.searchJitsi();
console.log("jitsi api ", this.jitsiApi);
console.log("iframe cowebsite", coWebsite?.iframe);
if (!coWebsite) {
this.destroy();
return;
}
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(coWebsite);
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(coWebsite);
});
}
public stop() {
@ -197,14 +247,16 @@ class JitsiFactory {
return;
}
const jitsiCoWebsite = coWebsiteManager.searchJitsi();
if (jitsiCoWebsite) {
coWebsiteManager.closeJitsi().catch((e) => console.error(e));
}
this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback);
}
public destroy() {
if (!this.jitsiApi) {
return;
}
this.stop();
this.jitsiApi?.dispose();
}

View file

@ -7,8 +7,7 @@ import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStor
export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void;
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility";
import { layoutManagerActionStore } from "../Stores/LayoutManagerStore";
import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError";
import { localUserStore } from "../Connexion/LocalUserStore";
@ -20,8 +19,6 @@ export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager;
constructor() {
@ -73,36 +70,12 @@ export class MediaManager {
});
}
public showGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.add("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
};
buttonCloseFrame.removeEventListener("click", () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.showGameOverlay();
public showMyCamera(): void {
myCameraVisibilityStore.set(true);
}
public hideGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.remove("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
};
buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.hideGameOverlay();
public hideMyCamera(): void {
myCameraVisibilityStore.set(false);
}
private getScreenSharingId(userId: string): string {
@ -179,20 +152,6 @@ export class MediaManager {
return connectingSpinnerDiv;
}
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
this.triggerCloseJistiFrame.set(id, Function);
}
public removeTriggerCloseJitsiFrameButton(id: String) {
this.triggerCloseJistiFrame.delete(id);
}
private triggerCloseJitsiFrameButton(): void {
for (const callback of this.triggerCloseJistiFrame.values()) {
callback();
}
}
public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager;
}

View file

@ -2,10 +2,10 @@ import type * as SimplePeerNamespace from "simple-peer";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { Readable, readable } from "svelte/store";
import { videoFocusStore } from "../Stores/VideoFocusStore";
import { Readable, readable, writable, Writable } from "svelte/store";
import { getIceServersConfig } from "../Components/Video/utils";
import { isMobile } from "../Enum/EnvironmentVariable";
import { highlightedEmbedScreen } from "../Stores/EmbedScreensStore";
import { isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -22,7 +22,7 @@ export class ScreenSharingPeer extends Peer {
public readonly userId: number;
public readonly uniqueId: string;
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
public readonly statusStore: Writable<PeerStatus>;
constructor(
user: UserSimplePeerInterface,
@ -43,7 +43,10 @@ export class ScreenSharingPeer extends Peer {
this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream | null) => {
videoFocusStore.focus(this);
highlightedEmbedScreen.highlight({
type: "streamable",
embed: this,
});
set(stream);
};
const onData = (chunk: Buffer) => {
@ -67,7 +70,7 @@ export class ScreenSharingPeer extends Peer {
};
});
this.statusStore = readable<PeerStatus>("connecting", (set) => {
this.statusStore = writable<PeerStatus>("connecting", (set) => {
const onConnect = () => {
set("connected");
};
@ -138,6 +141,12 @@ export class ScreenSharingPeer extends Peer {
if (!stream) {
this.isReceivingStream = false;
} else {
//Check if the peer connection is already connected status. In this case, the status store must be set to 'connected'.
//In the case or player A send stream and player B send a stream, it's same peer connection, also the status must be changed to connect.
//TODO add event listening when the stream is ready for displaying and change the status
if (this._connected) {
this.statusStore.set("connected");
}
this.isReceivingStream = true;
}
}
@ -177,7 +186,13 @@ export class ScreenSharingPeer extends Peer {
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream);
this.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true, isMobile: isMobile() }))
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT,
streamEnded: true,
isMobile: isMediaBreakpointUp("md"),
})
)
);
}
}

View file

@ -86,7 +86,7 @@ export class SimplePeer {
}
);
mediaManager.showGameOverlay();
mediaManager.showMyCamera();
//receive message start
this.Connection.webRtcStartMessageStream.subscribe((message: UserSimplePeerInterface) => {
@ -259,7 +259,7 @@ export class SimplePeer {
console.warn(
"closeScreenSharingConnection => Tried to close connection for user " +
userId +
" but could not find user"
" but could not find user or no peer connection started"
);
return;
}

View file

@ -9,7 +9,7 @@ import { localStreamStore, obtainedMediaConstraintStore, ObtainedMediaStreamCons
import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore, newChatMessageSubject } from "../Stores/ChatStore";
import { getIceServersConfig } from "../Components/Video/utils";
import { isMobile } from "../Enum/EnvironmentVariable";
import { isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -202,7 +202,7 @@ export class VideoPeer extends Peer {
JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT,
...constraints,
isMobile: isMobile(),
isMobile: isMediaBreakpointUp("md"),
})
)
);

View file

@ -0,0 +1,10 @@
import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "Während Unterhaltungen verringern",
},
message: "Sprachnachricht",
};
export default audio;

View file

@ -0,0 +1,22 @@
import type { Translation } from "../i18n-types";
const camera: NonNullable<Translation["camera"]> = {
enable: {
title: "Bitte schalte deine Kamera und dein Mikrofon ein.",
start: "Los gehts!",
},
help: {
title: "Zugriff auf Kamera / Mikrofon erforderlich",
permissionDenied: "Zugriff verweigert",
content: "Der Zugriff auf Kamera und Mikrofon muss im Browser freigegeben werden.",
firefoxContent:
'Bitte klicke auf "Diese Entscheidungen speichern" Schaltfläche um erneute Nachfragen nach der Freigabe in Firefox zu verhindern.',
refresh: "Aktualisieren",
continue: "Ohne Kamera fortfahren",
},
my: {
silentZone: "Stiller Bereich",
},
};
export default camera;

View file

@ -0,0 +1,12 @@
import type { Translation } from "../i18n-types";
const chat: NonNullable<Translation["chat"]> = {
intro: "Hier ist dein Nachrichtenverlauf:",
enter: "Verfasse deine Nachricht...",
menu: {
visitCard: "Visitenkarte",
addFriend: "Freund*In hinzufügen",
},
};
export default chat;

Some files were not shown because too many files have changed in this diff Show more