Merge pull request #1290 from thecodingmachine/develop
Deploy 2021-07-15
This commit is contained in:
commit
84948eb9da
@ -12,11 +12,17 @@
|
|||||||
- Use `WA.room.hideLayer(): void` to hide a layer
|
- Use `WA.room.hideLayer(): void` to hide a layer
|
||||||
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
||||||
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
||||||
- Use `WA.room.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
|
- Use `WA.player.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
|
||||||
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
|
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
|
||||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||||
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
||||||
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
||||||
|
- The text chat was redesigned to be prettier and to use more features :
|
||||||
|
- The chat is now persistent bewteen discussions and always accesible
|
||||||
|
- The chat now tracks incoming and outcoming users in your conversation
|
||||||
|
- The chat allows your to see the visit card of users
|
||||||
|
- You can close the chat window with the escape key
|
||||||
|
- Added a 'Enable notifications' button in the menu.
|
||||||
|
|
||||||
## Version 1.4.3 - 1.4.4 - 1.4.5
|
## Version 1.4.3 - 1.4.4 - 1.4.5
|
||||||
|
|
||||||
|
@ -15,12 +15,6 @@ import { Admin } from "../Model/Admin";
|
|||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
|
|
||||||
export enum GameRoomPolicyTypes {
|
|
||||||
ANONYMOUS_POLICY = 1,
|
|
||||||
MEMBERS_ONLY_POLICY,
|
|
||||||
USE_TAGS_POLICY,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GameRoom {
|
export class GameRoom {
|
||||||
private readonly minDistance: number;
|
private readonly minDistance: number;
|
||||||
private readonly groupRadius: number;
|
private readonly groupRadius: number;
|
||||||
|
@ -436,10 +436,7 @@ export class SocketManager {
|
|||||||
const serverToClientMessage1 = new ServerToClientMessage();
|
const serverToClientMessage1 = new ServerToClientMessage();
|
||||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
|
||||||
user.socket.write(serverToClientMessage1);
|
user.socket.write(serverToClientMessage1);
|
||||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
|
||||||
//}
|
|
||||||
|
|
||||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||||
webrtcStartMessage2.setUserid(user.id);
|
webrtcStartMessage2.setUserid(user.id);
|
||||||
@ -453,10 +450,7 @@ export class SocketManager {
|
|||||||
const serverToClientMessage2 = new ServerToClientMessage();
|
const serverToClientMessage2 = new ServerToClientMessage();
|
||||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
|
||||||
otherUser.socket.write(serverToClientMessage2);
|
otherUser.socket.write(serverToClientMessage2);
|
||||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{.section-title.accent.text-primary}
|
{.section-title.accent.text-primary}
|
||||||
# API Player functions Reference
|
# API Player functions Reference
|
||||||
|
|
||||||
|
|
||||||
### Listen to player movement
|
### Listen to player movement
|
||||||
```
|
```
|
||||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||||
|
@ -79,44 +79,6 @@ Example :
|
|||||||
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting information on the current room
|
|
||||||
```
|
|
||||||
WA.room.getCurrentRoom(): Promise<Room>
|
|
||||||
```
|
|
||||||
Return a promise that resolves to a `Room` object with the following attributes :
|
|
||||||
* **id (string) :** ID of the current room
|
|
||||||
* **map (ITiledMap) :** contains the JSON map file with the properties that were set by the script if `setProperty` was called.
|
|
||||||
* **mapUrl (string) :** Url of the JSON map file
|
|
||||||
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer
|
|
||||||
|
|
||||||
Example :
|
|
||||||
```javascript
|
|
||||||
WA.room.getCurrentRoom((room) => {
|
|
||||||
if (room.id === '42') {
|
|
||||||
console.log(room.map);
|
|
||||||
window.open(room.mapUrl, '_blank');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getting information on the current user
|
|
||||||
```
|
|
||||||
WA.player.getCurrentUser(): Promise<User>
|
|
||||||
```
|
|
||||||
Return a promise that resolves to a `User` object with the following attributes :
|
|
||||||
* **id (string) :** ID of the current user
|
|
||||||
* **nickName (string) :** name displayed above the current user
|
|
||||||
* **tags (string[]) :** list of all the tags of the current user
|
|
||||||
|
|
||||||
Example :
|
|
||||||
```javascript
|
|
||||||
WA.room.getCurrentUser().then((user) => {
|
|
||||||
if (user.nickName === 'ABC') {
|
|
||||||
console.log(user.tags);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changing tiles
|
### Changing tiles
|
||||||
```
|
```
|
||||||
WA.room.setTiles(tiles: TileDescriptor[]): void
|
WA.room.setTiles(tiles: TileDescriptor[]): void
|
||||||
|
3
front/dist/index.tmpl.html
vendored
3
front/dist/index.tmpl.html
vendored
@ -37,8 +37,7 @@
|
|||||||
<div class="main-container" id="main-container">
|
<div class="main-container" id="main-container">
|
||||||
<!-- Create the editor container -->
|
<!-- Create the editor container -->
|
||||||
<div id="game" class="game">
|
<div id="game" class="game">
|
||||||
<div id="svelte-overlay">
|
<div id="svelte-overlay"></div>
|
||||||
</div>
|
|
||||||
<div id="game-overlay" class="game-overlay">
|
<div id="game-overlay" class="game-overlay">
|
||||||
<div id="main-section" class="main-section">
|
<div id="main-section" class="main-section">
|
||||||
</div>
|
</div>
|
||||||
|
3
front/dist/resources/html/gameMenu.html
vendored
3
front/dist/resources/html/gameMenu.html
vendored
@ -57,6 +57,9 @@
|
|||||||
<section>
|
<section>
|
||||||
<button id="toggleFullscreen">Toggle fullscreen</button>
|
<button id="toggleFullscreen">Toggle fullscreen</button>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<button id="enableNotification">Enable notifications</button>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<button id="sparkButton">Create map</button>
|
<button id="sparkButton">Create map</button>
|
||||||
</section>
|
</section>
|
||||||
|
BIN
front/dist/static/images/send.png
vendored
Normal file
BIN
front/dist/static/images/send.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/press-start-2p": "^4.3.0",
|
"@fontsource/press-start-2p": "^4.3.0",
|
||||||
"@types/simple-peer": "^9.6.0",
|
"@types/simple-peer": "^9.11.1",
|
||||||
"@types/socket.io-client": "^1.4.32",
|
"@types/socket.io-client": "^1.4.32",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"queue-typescript": "^1.0.1",
|
"queue-typescript": "^1.0.1",
|
||||||
"quill": "1.3.6",
|
"quill": "1.3.6",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"simple-peer": "^9.6.2",
|
"simple-peer": "^9.11.0",
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"standardized-audio-context": "^25.2.4"
|
"standardized-audio-context": "^25.2.4"
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,15 @@ import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribut
|
|||||||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
import { apiCallback } from "./registeredCallbacks";
|
import { apiCallback } from "./registeredCallbacks";
|
||||||
|
import { getGameState } from "./room";
|
||||||
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string | undefined;
|
||||||
|
nickName: string | null;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||||
|
|
||||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||||
@ -24,6 +31,11 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||||||
data: null,
|
data: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getCurrentUser(): Promise<User> {
|
||||||
|
return getGameState().then((gameState) => {
|
||||||
|
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WorkadventurePlayerCommands();
|
export default new WorkadventurePlayerCommands();
|
||||||
|
@ -14,7 +14,6 @@ import type { GameStateEvent } from "../Events/GameStateEvent";
|
|||||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||||
const dataLayerResolver = new Subject<DataLayerEvent>();
|
const dataLayerResolver = new Subject<DataLayerEvent>();
|
||||||
const stateResolvers = new Subject<GameStateEvent>();
|
|
||||||
|
|
||||||
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
||||||
|
|
||||||
@ -25,12 +24,6 @@ interface Room {
|
|||||||
startLayer: string | null;
|
startLayer: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string | undefined;
|
|
||||||
nickName: string | null;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TileDescriptor {
|
interface TileDescriptor {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@ -38,7 +31,7 @@ interface TileDescriptor {
|
|||||||
layer: string;
|
layer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGameState(): Promise<GameStateEvent> {
|
export function getGameState(): Promise<GameStateEvent> {
|
||||||
if (immutableDataPromise === undefined) {
|
if (immutableDataPromise === undefined) {
|
||||||
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
||||||
}
|
}
|
||||||
@ -121,11 +114,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
getCurrentUser(): Promise<User> {
|
|
||||||
return getGameState().then((gameState) => {
|
|
||||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setTiles(tiles: TileDescriptor[]) {
|
setTiles(tiles: TileDescriptor[]) {
|
||||||
sendToWorkadventure({
|
sendToWorkadventure({
|
||||||
type: "setTiles",
|
type: "setTiles",
|
||||||
|
@ -10,12 +10,14 @@
|
|||||||
import {errorStore} from "../Stores/ErrorStore";
|
import {errorStore} from "../Stores/ErrorStore";
|
||||||
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
||||||
import LoginScene from "./Login/LoginScene.svelte";
|
import LoginScene from "./Login/LoginScene.svelte";
|
||||||
|
import Chat from "./Chat/Chat.svelte";
|
||||||
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
||||||
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
||||||
import VisitCard from "./VisitCard/VisitCard.svelte";
|
import VisitCard from "./VisitCard/VisitCard.svelte";
|
||||||
import {requestVisitCardsStore} from "../Stores/GameStore";
|
import {requestVisitCardsStore} from "../Stores/GameStore";
|
||||||
|
|
||||||
import type {Game} from "../Phaser/Game/Game";
|
import type {Game} from "../Phaser/Game/Game";
|
||||||
|
import {chatVisibilityStore} from "../Stores/ChatStore";
|
||||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||||
@ -61,14 +63,6 @@
|
|||||||
<AudioPlaying url={$soundPlayingStore} />
|
<AudioPlaying url={$soundPlayingStore} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!--
|
|
||||||
{#if $menuIconVisible}
|
|
||||||
<div>
|
|
||||||
<MenuIcon />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
-->
|
|
||||||
{#if $gameOverlayVisibilityStore}
|
{#if $gameOverlayVisibilityStore}
|
||||||
<div>
|
<div>
|
||||||
<VideoOverlay></VideoOverlay>
|
<VideoOverlay></VideoOverlay>
|
||||||
@ -94,4 +88,7 @@
|
|||||||
<ErrorDialog></ErrorDialog>
|
<ErrorDialog></ErrorDialog>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $chatVisibilityStore}
|
||||||
|
<Chat></Chat>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
104
front/src/Components/Chat/Chat.svelte
Normal file
104
front/src/Components/Chat/Chat.svelte
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
import ChatMessageForm from './ChatMessageForm.svelte';
|
||||||
|
import ChatElement from './ChatElement.svelte';
|
||||||
|
import { afterUpdate, beforeUpdate } from "svelte";
|
||||||
|
|
||||||
|
let listDom: HTMLElement;
|
||||||
|
let autoscroll: boolean;
|
||||||
|
|
||||||
|
beforeUpdate(() => {
|
||||||
|
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeChat() {
|
||||||
|
chatVisibilityStore.set(false);
|
||||||
|
}
|
||||||
|
function onKeyDown(e:KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeChat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={onKeyDown}/>
|
||||||
|
|
||||||
|
|
||||||
|
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||||
|
<section class="chatWindowTitle">
|
||||||
|
<h1>Your chat history <span class="float-right" on:click={closeChat}>×</span></h1>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section class="messagesList" bind:this={listDom}>
|
||||||
|
<ul>
|
||||||
|
{#each $chatMessagesStore as message, i}
|
||||||
|
<li><ChatElement message={message} line={i}></ChatElement></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="messageForm">
|
||||||
|
<ChatMessageForm></ChatMessageForm>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
font-family: Lato;
|
||||||
|
|
||||||
|
span.float-right {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.chatWindow {
|
||||||
|
z-index:100;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width:30vw;
|
||||||
|
min-width: 350px;
|
||||||
|
background: rgb(5, 31, 51, 0.9);
|
||||||
|
color: whitesmoke;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
border-top-right-radius: 16px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
background-color: #5f5f5f;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatWindowTitle {
|
||||||
|
flex: 0 100px;
|
||||||
|
}
|
||||||
|
.messagesList {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.messageForm {
|
||||||
|
flex: 0 70px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
83
front/src/Components/Chat/ChatElement.svelte
Normal file
83
front/src/Components/Chat/ChatElement.svelte
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {ChatMessageTypes} from "../../Stores/ChatStore";
|
||||||
|
import type {ChatMessage} from "../../Stores/ChatStore";
|
||||||
|
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||||
|
import ChatPlayerName from './ChatPlayerName.svelte';
|
||||||
|
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||||
|
|
||||||
|
export let message: ChatMessage;
|
||||||
|
export let line: number;
|
||||||
|
|
||||||
|
$: author = message.author as PlayerInterface;
|
||||||
|
$: targets = message.targets || [];
|
||||||
|
$: texts = message.text || [];
|
||||||
|
|
||||||
|
function urlifyText(text: string): string {
|
||||||
|
return HtmlUtils.urlify(text)
|
||||||
|
}
|
||||||
|
function renderDate(date: Date) {
|
||||||
|
return date.toLocaleTimeString(navigator.language, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute:'2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function isLastIteration(index: number) {
|
||||||
|
return targets.length -1 === index;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chatElement">
|
||||||
|
<div class="messagePart">
|
||||||
|
{#if message.type === ChatMessageTypes.userIncoming}
|
||||||
|
>> {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
|
||||||
|
{:else if message.type === ChatMessageTypes.userOutcoming}
|
||||||
|
<< {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
|
||||||
|
{:else if message.type === ChatMessageTypes.me}
|
||||||
|
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
|
||||||
|
{#each texts as text}
|
||||||
|
<div><p class="my-text">{@html urlifyText(text)}</p></div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
|
||||||
|
{#each texts as text}
|
||||||
|
<div><p class="other-text">{@html urlifyText(text)}</p></div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h4, p {
|
||||||
|
font-family: Lato;
|
||||||
|
}
|
||||||
|
div.chatElement {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.messagePart {
|
||||||
|
flex-grow:1;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
span.date {
|
||||||
|
font-size: 80%;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
div > p {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding:6px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
&.other-text {
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.my-text {
|
||||||
|
background: #6489ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
56
front/src/Components/Chat/ChatMessageForm.svelte
Normal file
56
front/src/Components/Chat/ChatMessageForm.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
|
||||||
|
|
||||||
|
let newMessageText = '';
|
||||||
|
|
||||||
|
function onFocus() {
|
||||||
|
chatInputFocusStore.set(true);
|
||||||
|
}
|
||||||
|
function onBlur() {
|
||||||
|
chatInputFocusStore.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveMessage() {
|
||||||
|
if (!newMessageText) return;
|
||||||
|
chatMessagesStore.addPersonnalMessage(newMessageText);
|
||||||
|
newMessageText = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={saveMessage}>
|
||||||
|
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
|
||||||
|
<button type="submit">
|
||||||
|
<img src="/static/images/send.png" alt="Send" width="20">
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: auto;
|
||||||
|
background-color: #42464d;
|
||||||
|
color: white;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: Lato;
|
||||||
|
padding-left: 6px;
|
||||||
|
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #42464d;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
border-left: solid black 1px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
51
front/src/Components/Chat/ChatPlayerName.svelte
Normal file
51
front/src/Components/Chat/ChatPlayerName.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||||
|
import {chatSubMenuVisbilityStore} from "../../Stores/ChatStore";
|
||||||
|
import {onDestroy, onMount} from "svelte";
|
||||||
|
import type {Unsubscriber} from "svelte/store";
|
||||||
|
import ChatSubMenu from "./ChatSubMenu.svelte";
|
||||||
|
|
||||||
|
export let player: PlayerInterface;
|
||||||
|
export let line: number;
|
||||||
|
|
||||||
|
let isSubMenuOpen: boolean;
|
||||||
|
let chatSubMenuVisivilytUnsubcribe: Unsubscriber;
|
||||||
|
|
||||||
|
function openSubMenu() {
|
||||||
|
chatSubMenuVisbilityStore.openSubMenu(player.name, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisbilityStore.subscribe((newValue) => {
|
||||||
|
isSubMenuOpen = (newValue === player.name + line);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
chatSubMenuVisivilytUnsubcribe();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="subMenu">
|
||||||
|
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
|
||||||
|
{player.name}
|
||||||
|
</span>
|
||||||
|
{#if isSubMenuOpen}
|
||||||
|
<ChatSubMenu player={player}/>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
span.subMenu {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
span.chatPlayerName {
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
.chatPlayerName:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
33
front/src/Components/Chat/ChatSubMenu.svelte
Normal file
33
front/src/Components/Chat/ChatSubMenu.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||||
|
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||||
|
|
||||||
|
export let player: PlayerInterface;
|
||||||
|
|
||||||
|
|
||||||
|
function openVisitCard() {
|
||||||
|
if (player.visitCardUrl) {
|
||||||
|
requestVisitCardsStore.set(player.visitCardUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
|
||||||
|
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li>
|
||||||
|
<li><button class="text-btn" disabled>Add friend</button></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
ul.selectMenu {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -37,9 +37,7 @@
|
|||||||
<img alt="Report this user" src={reportImg}>
|
<img alt="Report this user" src={reportImg}>
|
||||||
<span>Report/Block</span>
|
<span>Report/Block</span>
|
||||||
</button>
|
</button>
|
||||||
{#if $streamStore }
|
|
||||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||||
{/if}
|
|
||||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||||
{#if $constraintStore && $constraintStore.audio !== false}
|
{#if $constraintStore && $constraintStore.audio !== false}
|
||||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import type { UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
|
||||||
|
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
export function getColorByString(str: string): string | null {
|
export function getColorByString(str: string): string | null {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
@ -15,7 +18,7 @@ export function getColorByString(str: string): string | null {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
|
||||||
node.srcObject = stream;
|
node.srcObject = stream;
|
||||||
return {
|
return {
|
||||||
update(newStream: MediaStream) {
|
update(newStream: MediaStream) {
|
||||||
@ -25,3 +28,19 @@ export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIceServersConfig(user: UserSimplePeerInterface): RTCIceServer[] {
|
||||||
|
const config: RTCIceServer[] = [
|
||||||
|
{
|
||||||
|
urls: STUN_SERVER.split(","),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (TURN_SERVER !== "") {
|
||||||
|
config.push({
|
||||||
|
urls: TURN_SERVER.split(","),
|
||||||
|
username: user.webRtcUser || TURN_USER,
|
||||||
|
credential: user.webRtcPassword || TURN_PASSWORD,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
@ -45,8 +45,9 @@
|
|||||||
|
|
||||||
.visitCard {
|
.visitCard {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
margin-left: auto;
|
position: absolute;
|
||||||
margin-right: auto;
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
margin-top: 200px;
|
margin-top: 200px;
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {discussionManager} from "../../WebRtc/DiscussionManager";
|
|
||||||
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
||||||
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
export const openChatIconName = 'openChatIcon';
|
export const openChatIconName = "openChatIcon";
|
||||||
export class OpenChatIcon extends Phaser.GameObjects.Image {
|
export class OpenChatIcon extends Phaser.GameObjects.Image {
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
super(scene, x, y, openChatIconName, 3);
|
super(scene, x, y, openChatIconName, 3);
|
||||||
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
|
|||||||
this.setScrollFactor(0, 0);
|
this.setScrollFactor(0, 0);
|
||||||
this.setOrigin(0, 1);
|
this.setOrigin(0, 1);
|
||||||
this.setInteractive();
|
this.setInteractive();
|
||||||
this.setVisible(false);
|
//this.setVisible(false);
|
||||||
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||||
|
|
||||||
this.on("pointerup", () => discussionManager.showDiscussionPart());
|
this.on("pointerup", () => chatVisibilityStore.set(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -101,7 +101,6 @@ export const createLoadingPromise = (
|
|||||||
frameConfig: FrameConfig
|
frameConfig: FrameConfig
|
||||||
) => {
|
) => {
|
||||||
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
|
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
|
||||||
console.log("count", loadPlugin.listenerCount("loaderror"));
|
|
||||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||||
return res(playerResourceDescriptor);
|
return res(playerResourceDescriptor);
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,7 @@ import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
|||||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||||
import { playersStore } from "../../Stores/PlayersStore";
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
@ -169,6 +170,7 @@ export class GameScene extends DirtyScene {
|
|||||||
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
|
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
|
||||||
private iframeSubscriptionList!: Array<Subscription>;
|
private iframeSubscriptionList!: Array<Subscription>;
|
||||||
private peerStoreUnsubscribe!: () => void;
|
private peerStoreUnsubscribe!: () => void;
|
||||||
|
private chatVisibilityUnsubscribe!: () => void;
|
||||||
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
||||||
MapUrlFile: string;
|
MapUrlFile: string;
|
||||||
RoomId: string;
|
RoomId: string;
|
||||||
@ -571,6 +573,10 @@ export class GameScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
oldPeerNumber = newPeerNumber;
|
oldPeerNumber = newPeerNumber;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => {
|
||||||
|
this.openChatIcon.setVisible(!v);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -692,12 +698,12 @@ export class GameScene extends DirtyScene {
|
|||||||
const self = this;
|
const self = this;
|
||||||
this.simplePeer.registerPeerConnectionListener({
|
this.simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(peer) {
|
onConnect(peer) {
|
||||||
self.openChatIcon.setVisible(true);
|
//self.openChatIcon.setVisible(true);
|
||||||
audioManager.decreaseVolume();
|
audioManager.decreaseVolume();
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
if (self.simplePeer.getNbConnections() === 0) {
|
if (self.simplePeer.getNbConnections() === 0) {
|
||||||
self.openChatIcon.setVisible(false);
|
//self.openChatIcon.setVisible(false);
|
||||||
audioManager.restoreVolume();
|
audioManager.restoreVolume();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1173,6 +1179,7 @@ ${escapedMessage}
|
|||||||
this.pinchManager?.destroy();
|
this.pinchManager?.destroy();
|
||||||
this.emoteManager.destroy();
|
this.emoteManager.destroy();
|
||||||
this.peerStoreUnsubscribe();
|
this.peerStoreUnsubscribe();
|
||||||
|
this.chatVisibilityUnsubscribe();
|
||||||
this.biggestAvailableAreaStoreUnsubscribe();
|
this.biggestAvailableAreaStoreUnsubscribe();
|
||||||
iframeListener.unregisterAnswerer("getState");
|
iframeListener.unregisterAnswerer("getState");
|
||||||
|
|
||||||
|
@ -7,4 +7,5 @@ export interface PlayerInterface {
|
|||||||
visitCardUrl: string | null;
|
visitCardUrl: string | null;
|
||||||
companion: string | null;
|
companion: string | null;
|
||||||
userUuid: string;
|
userUuid: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
|
|||||||
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { playersStore } from "../../Stores/PlayersStore";
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
|
import { mediaManager } from "../../WebRtc/MediaManager";
|
||||||
|
|
||||||
export const MenuSceneName = "MenuScene";
|
export const MenuSceneName = "MenuScene";
|
||||||
const gameMenuKey = "gameMenu";
|
const gameMenuKey = "gameMenu";
|
||||||
@ -98,6 +99,10 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
this.menuElement.setOrigin(0);
|
this.menuElement.setOrigin(0);
|
||||||
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
|
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
|
||||||
|
|
||||||
|
if (mediaManager.hasNotification()) {
|
||||||
|
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
const middleX = window.innerWidth / 3 - 298;
|
const middleX = window.innerWidth / 3 - 298;
|
||||||
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
|
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
|
||||||
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
|
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
|
||||||
@ -357,6 +362,9 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
case "toggleFullscreen":
|
case "toggleFullscreen":
|
||||||
this.toggleFullscreen();
|
this.toggleFullscreen();
|
||||||
break;
|
break;
|
||||||
|
case "enableNotification":
|
||||||
|
this.enableNotification();
|
||||||
|
break;
|
||||||
case "adminConsoleButton":
|
case "adminConsoleButton":
|
||||||
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
if (get(consoleGlobalMessageManagerVisibleStore)) {
|
||||||
consoleGlobalMessageManagerVisibleStore.set(false);
|
consoleGlobalMessageManagerVisibleStore.set(false);
|
||||||
@ -419,4 +427,12 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
public isDirty(): boolean {
|
public isDirty(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enableNotification() {
|
||||||
|
mediaManager.requestNotification().then(() => {
|
||||||
|
if (mediaManager.hasNotification()) {
|
||||||
|
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
118
front/src/Stores/ChatStore.ts
Normal file
118
front/src/Stores/ChatStore.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { playersStore } from "./PlayersStore";
|
||||||
|
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
||||||
|
|
||||||
|
export const chatVisibilityStore = writable(false);
|
||||||
|
export const chatInputFocusStore = writable(false);
|
||||||
|
|
||||||
|
export const newChatMessageStore = writable<string | null>(null);
|
||||||
|
|
||||||
|
export enum ChatMessageTypes {
|
||||||
|
text = 1,
|
||||||
|
me,
|
||||||
|
userIncoming,
|
||||||
|
userOutcoming,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
type: ChatMessageTypes;
|
||||||
|
date: Date;
|
||||||
|
author?: PlayerInterface;
|
||||||
|
targets?: PlayerInterface[];
|
||||||
|
text?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthor(authorId: number): PlayerInterface {
|
||||||
|
const author = playersStore.getPlayerById(authorId);
|
||||||
|
if (!author) {
|
||||||
|
throw "Could not find data for author " + authorId;
|
||||||
|
}
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChatMessagesStore() {
|
||||||
|
const { subscribe, update } = writable<ChatMessage[]>([]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
addIncomingUser(authorId: number) {
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.userIncoming && lastMessage.targets) {
|
||||||
|
lastMessage.targets.push(getAuthor(authorId));
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.userIncoming,
|
||||||
|
targets: [getAuthor(authorId)],
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addOutcomingUser(authorId: number) {
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.userOutcoming && lastMessage.targets) {
|
||||||
|
lastMessage.targets.push(getAuthor(authorId));
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.userOutcoming,
|
||||||
|
targets: [getAuthor(authorId)],
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addPersonnalMessage(text: string) {
|
||||||
|
newChatMessageStore.set(text);
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.me && lastMessage.text) {
|
||||||
|
lastMessage.text.push(text);
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.me,
|
||||||
|
text: [text],
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addExternalMessage(authorId: number, text: string) {
|
||||||
|
update((list) => {
|
||||||
|
const lastMessage = list[list.length - 1];
|
||||||
|
if (lastMessage && lastMessage.type === ChatMessageTypes.text && lastMessage.text) {
|
||||||
|
lastMessage.text.push(text);
|
||||||
|
} else {
|
||||||
|
list.push({
|
||||||
|
type: ChatMessageTypes.text,
|
||||||
|
text: [text],
|
||||||
|
author: getAuthor(authorId),
|
||||||
|
date: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const chatMessagesStore = createChatMessagesStore();
|
||||||
|
|
||||||
|
function createChatSubMenuVisibilityStore() {
|
||||||
|
const { subscribe, update } = writable<string>("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
openSubMenu(playerName: string, index: number) {
|
||||||
|
const id = playerName + index;
|
||||||
|
update((oldValue) => {
|
||||||
|
return oldValue === id ? "" : id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatSubMenuVisbilityStore = createChatSubMenuVisibilityStore();
|
@ -1,6 +1,7 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
||||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
|
import { getRandomColor } from "../WebRtc/ColorGenerator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the list of players currently known.
|
* A store that contains the list of players currently known.
|
||||||
@ -24,6 +25,7 @@ function createPlayersStore() {
|
|||||||
visitCardUrl: message.visitCardUrl,
|
visitCardUrl: message.visitCardUrl,
|
||||||
companion: message.companion,
|
companion: message.companion,
|
||||||
userUuid: message.userUuid,
|
userUuid: message.userUuid,
|
||||||
|
color: getRandomColor(),
|
||||||
});
|
});
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { derived } from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
|
import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
|
||||||
|
import { chatInputFocusStore } from "./ChatStore";
|
||||||
|
|
||||||
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
|
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
|
||||||
export const enableUserInputsStore = derived(
|
export const enableUserInputsStore = derived(
|
||||||
consoleGlobalMessageManagerFocusStore,
|
[consoleGlobalMessageManagerFocusStore, chatInputFocusStore],
|
||||||
($consoleGlobalMessageManagerFocusStore) => {
|
([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => {
|
||||||
return !$consoleGlobalMessageManagerFocusStore;
|
return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore;
|
||||||
}
|
}
|
||||||
);
|
);
|
52
front/src/WebRtc/ColorGenerator.ts
Normal file
52
front/src/WebRtc/ColorGenerator.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export function getRandomColor(): string {
|
||||||
|
const golden_ratio_conjugate = 0.618033988749895;
|
||||||
|
let hue = Math.random();
|
||||||
|
hue += golden_ratio_conjugate;
|
||||||
|
hue %= 1;
|
||||||
|
return hsv_to_rgb(hue, 0.5, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo: test this.
|
||||||
|
function hsv_to_rgb(hue: number, saturation: number, brightness: number): string {
|
||||||
|
const h_i = Math.floor(hue * 6);
|
||||||
|
const f = hue * 6 - h_i;
|
||||||
|
const p = brightness * (1 - saturation);
|
||||||
|
const q = brightness * (1 - f * saturation);
|
||||||
|
const t = brightness * (1 - (1 - f) * saturation);
|
||||||
|
let r: number, g: number, b: number;
|
||||||
|
switch (h_i) {
|
||||||
|
case 0:
|
||||||
|
r = brightness;
|
||||||
|
g = t;
|
||||||
|
b = p;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
r = q;
|
||||||
|
g = brightness;
|
||||||
|
b = p;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
r = p;
|
||||||
|
g = brightness;
|
||||||
|
b = t;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
r = p;
|
||||||
|
g = q;
|
||||||
|
b = brightness;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
r = t;
|
||||||
|
g = p;
|
||||||
|
b = brightness;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
r = brightness;
|
||||||
|
g = p;
|
||||||
|
b = q;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "h_i cannot be " + h_i;
|
||||||
|
}
|
||||||
|
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
|
||||||
|
}
|
@ -1,232 +1,12 @@
|
|||||||
import { HtmlUtils } from "./HtmlUtils";
|
|
||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
|
||||||
import { connectionManager } from "../Connexion/ConnectionManager";
|
|
||||||
import { GameConnexionTypes } from "../Url/UrlManager";
|
|
||||||
import { iframeListener } from "../Api/IframeListener";
|
import { iframeListener } from "../Api/IframeListener";
|
||||||
import { showReportScreenStore } from "../Stores/ShowReportScreenStore";
|
import { chatMessagesStore, chatVisibilityStore } from "../Stores/ChatStore";
|
||||||
|
|
||||||
export type SendMessageCallback = (message: string) => void;
|
|
||||||
|
|
||||||
export class DiscussionManager {
|
export class DiscussionManager {
|
||||||
private mainContainer: HTMLDivElement;
|
|
||||||
|
|
||||||
private divDiscuss?: HTMLDivElement;
|
|
||||||
private divParticipants?: HTMLDivElement;
|
|
||||||
private nbpParticipants?: HTMLParagraphElement;
|
|
||||||
private divMessages?: HTMLParagraphElement;
|
|
||||||
|
|
||||||
private participants: Map<number | string, HTMLDivElement> = new Map<number | string, HTMLDivElement>();
|
|
||||||
|
|
||||||
private activeDiscussion: boolean = false;
|
|
||||||
|
|
||||||
private sendMessageCallBack: Map<number | string, SendMessageCallback> = new Map<
|
|
||||||
number | string,
|
|
||||||
SendMessageCallback
|
|
||||||
>();
|
|
||||||
|
|
||||||
private userInputManager?: UserInputManager;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
|
||||||
this.createDiscussPart(""); //todo: why do we always use empty string?
|
|
||||||
|
|
||||||
iframeListener.chatStream.subscribe((chatEvent) => {
|
iframeListener.chatStream.subscribe((chatEvent) => {
|
||||||
this.addMessage(chatEvent.author, chatEvent.message, false);
|
chatMessagesStore.addExternalMessage(parseInt(chatEvent.author), chatEvent.message);
|
||||||
this.showDiscussion();
|
chatVisibilityStore.set(true);
|
||||||
});
|
});
|
||||||
this.onSendMessageCallback("iframe_listener", (message) => {
|
|
||||||
iframeListener.sendUserInputChat(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createDiscussPart(name: string) {
|
|
||||||
this.divDiscuss = document.createElement("div");
|
|
||||||
this.divDiscuss.classList.add("discussion");
|
|
||||||
|
|
||||||
const buttonCloseDiscussion: HTMLButtonElement = document.createElement("button");
|
|
||||||
buttonCloseDiscussion.classList.add("close-btn");
|
|
||||||
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
|
|
||||||
buttonCloseDiscussion.addEventListener("click", () => {
|
|
||||||
this.hideDiscussion();
|
|
||||||
});
|
|
||||||
this.divDiscuss.appendChild(buttonCloseDiscussion);
|
|
||||||
|
|
||||||
const myName: HTMLParagraphElement = document.createElement("p");
|
|
||||||
myName.innerText = name.toUpperCase();
|
|
||||||
this.nbpParticipants = document.createElement("p");
|
|
||||||
this.nbpParticipants.innerText = "PARTICIPANTS (1)";
|
|
||||||
|
|
||||||
this.divParticipants = document.createElement("div");
|
|
||||||
this.divParticipants.classList.add("participants");
|
|
||||||
|
|
||||||
this.divMessages = document.createElement("div");
|
|
||||||
this.divMessages.classList.add("messages");
|
|
||||||
this.divMessages.innerHTML = "<h2>Local messages</h2>";
|
|
||||||
|
|
||||||
this.divDiscuss.appendChild(myName);
|
|
||||||
this.divDiscuss.appendChild(this.nbpParticipants);
|
|
||||||
this.divDiscuss.appendChild(this.divParticipants);
|
|
||||||
this.divDiscuss.appendChild(this.divMessages);
|
|
||||||
|
|
||||||
const sendDivMessage: HTMLDivElement = document.createElement("div");
|
|
||||||
sendDivMessage.classList.add("send-message");
|
|
||||||
const inputMessage: HTMLInputElement = document.createElement("input");
|
|
||||||
inputMessage.onfocus = () => {
|
|
||||||
if (this.userInputManager) {
|
|
||||||
this.userInputManager.disableControls();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
inputMessage.onblur = () => {
|
|
||||||
if (this.userInputManager) {
|
|
||||||
this.userInputManager.restoreControls();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
inputMessage.type = "text";
|
|
||||||
inputMessage.addEventListener("keyup", (event: KeyboardEvent) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
if (inputMessage.value === null || inputMessage.value === "" || inputMessage.value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.addMessage(name, inputMessage.value, true);
|
|
||||||
for (const callback of this.sendMessageCallBack.values()) {
|
|
||||||
callback(inputMessage.value);
|
|
||||||
}
|
|
||||||
inputMessage.value = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sendDivMessage.appendChild(inputMessage);
|
|
||||||
this.divDiscuss.appendChild(sendDivMessage);
|
|
||||||
|
|
||||||
//append in main container
|
|
||||||
this.mainContainer.appendChild(this.divDiscuss);
|
|
||||||
|
|
||||||
this.addParticipant("me", "Moi", undefined, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addParticipant(
|
|
||||||
userId: number | "me",
|
|
||||||
name: string | undefined,
|
|
||||||
img?: string | undefined,
|
|
||||||
isMe: boolean = false
|
|
||||||
) {
|
|
||||||
const divParticipant: HTMLDivElement = document.createElement("div");
|
|
||||||
divParticipant.classList.add("participant");
|
|
||||||
divParticipant.id = `participant-${userId}`;
|
|
||||||
|
|
||||||
const divImgParticipant: HTMLImageElement = document.createElement("img");
|
|
||||||
divImgParticipant.src = "resources/logos/boy.svg";
|
|
||||||
if (img !== undefined) {
|
|
||||||
divImgParticipant.src = img;
|
|
||||||
}
|
|
||||||
const divPParticipant: HTMLParagraphElement = document.createElement("p");
|
|
||||||
if (!name) {
|
|
||||||
name = "Anonymous";
|
|
||||||
}
|
|
||||||
divPParticipant.innerText = name;
|
|
||||||
|
|
||||||
divParticipant.appendChild(divImgParticipant);
|
|
||||||
divParticipant.appendChild(divPParticipant);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isMe &&
|
|
||||||
connectionManager.getConnexionType &&
|
|
||||||
connectionManager.getConnexionType !== GameConnexionTypes.anonymous &&
|
|
||||||
userId !== "me"
|
|
||||||
) {
|
|
||||||
const reportBanUserAction: HTMLButtonElement = document.createElement("button");
|
|
||||||
reportBanUserAction.classList.add("report-btn");
|
|
||||||
reportBanUserAction.innerText = "Report";
|
|
||||||
reportBanUserAction.addEventListener("click", () => {
|
|
||||||
showReportScreenStore.set({ userId: userId, userName: name ? name : "" });
|
|
||||||
});
|
|
||||||
divParticipant.appendChild(reportBanUserAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.divParticipants?.appendChild(divParticipant);
|
|
||||||
|
|
||||||
this.participants.set(userId, divParticipant);
|
|
||||||
|
|
||||||
this.updateParticipant(this.participants.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateParticipant(nb: number) {
|
|
||||||
if (!this.nbpParticipants) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addMessage(name: string, message: string, isMe: boolean = false) {
|
|
||||||
const divMessage: HTMLDivElement = document.createElement("div");
|
|
||||||
divMessage.classList.add("message");
|
|
||||||
if (isMe) {
|
|
||||||
divMessage.classList.add("me");
|
|
||||||
}
|
|
||||||
|
|
||||||
const pMessage: HTMLParagraphElement = document.createElement("p");
|
|
||||||
const date = new Date();
|
|
||||||
if (isMe) {
|
|
||||||
name = "Me";
|
|
||||||
} else {
|
|
||||||
name = HtmlUtils.escapeHtml(name);
|
|
||||||
}
|
|
||||||
pMessage.innerHTML = `<span style="font-weight: bold">${name}</span>
|
|
||||||
<span style="color:#bac2cc;display:inline-block;font-size:12px;">
|
|
||||||
${date.getHours()}:${date.getMinutes()}
|
|
||||||
</span>`;
|
|
||||||
divMessage.appendChild(pMessage);
|
|
||||||
|
|
||||||
const userMessage: HTMLParagraphElement = document.createElement("p");
|
|
||||||
userMessage.innerHTML = HtmlUtils.urlify(message);
|
|
||||||
userMessage.classList.add("body");
|
|
||||||
divMessage.appendChild(userMessage);
|
|
||||||
this.divMessages?.appendChild(divMessage);
|
|
||||||
|
|
||||||
//automatic scroll when there are new message
|
|
||||||
setTimeout(() => {
|
|
||||||
this.divMessages?.scroll({
|
|
||||||
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeParticipant(userId: number | string) {
|
|
||||||
const element = this.participants.get(userId);
|
|
||||||
if (element) {
|
|
||||||
element.remove();
|
|
||||||
this.participants.delete(userId);
|
|
||||||
}
|
|
||||||
//if all participant leave, hide discussion button
|
|
||||||
|
|
||||||
this.sendMessageCallBack.delete(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSendMessageCallback(userId: string | number, callback: SendMessageCallback): void {
|
|
||||||
this.sendMessageCallBack.set(userId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
get activatedDiscussion() {
|
|
||||||
return this.activeDiscussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
private showDiscussion() {
|
|
||||||
this.activeDiscussion = true;
|
|
||||||
this.divDiscuss?.classList.add("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
private hideDiscussion() {
|
|
||||||
this.activeDiscussion = false;
|
|
||||||
this.divDiscuss?.classList.remove("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
public setUserInputManager(userInputManager: UserInputManager) {
|
|
||||||
this.userInputManager = userInputManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public showDiscussionPart() {
|
|
||||||
this.showDiscussion();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
import { DivImportance, layoutManager } from "./LayoutManager";
|
import { layoutManager } from "./LayoutManager";
|
||||||
import { HtmlUtils } from "./HtmlUtils";
|
import { HtmlUtils } from "./HtmlUtils";
|
||||||
import { discussionManager, SendMessageCallback } from "./DiscussionManager";
|
|
||||||
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
|
||||||
import { localUserStore } from "../Connexion/LocalUserStore";
|
|
||||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
|
||||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
|
||||||
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
|
|
||||||
import { localStreamStore } from "../Stores/MediaStore";
|
import { localStreamStore } from "../Stores/MediaStore";
|
||||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||||
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
||||||
|
|
||||||
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
|
|
||||||
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
||||||
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||||
|
|
||||||
@ -21,16 +15,11 @@ export class MediaManager {
|
|||||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||||
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||||
|
|
||||||
private focused: boolean = true;
|
|
||||||
|
|
||||||
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
|
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
|
||||||
|
|
||||||
private userInputManager?: UserInputManager;
|
private userInputManager?: UserInputManager;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
//Check of ask notification navigator permission
|
|
||||||
this.getNotification();
|
|
||||||
|
|
||||||
localStreamStore.subscribe((result) => {
|
localStreamStore.subscribe((result) => {
|
||||||
if (result.type === "error") {
|
if (result.type === "error") {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
@ -182,67 +171,35 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addNewMessage(name: string, message: string, isMe: boolean = false) {
|
|
||||||
discussionManager.addMessage(name, message, isMe);
|
|
||||||
|
|
||||||
//when there are new message, show discussion
|
|
||||||
if (!discussionManager.activatedDiscussion) {
|
|
||||||
discussionManager.showDiscussionPart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
|
|
||||||
discussionManager.onSendMessageCallback(userId, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setUserInputManager(userInputManager: UserInputManager) {
|
public setUserInputManager(userInputManager: UserInputManager) {
|
||||||
this.userInputManager = userInputManager;
|
this.userInputManager = userInputManager;
|
||||||
discussionManager.setUserInputManager(userInputManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNotification() {
|
public hasNotification(): boolean {
|
||||||
//Get notification
|
return Notification.permission === "granted";
|
||||||
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
|
}
|
||||||
if (this.checkNotificationPromise()) {
|
|
||||||
Notification.requestPermission().catch((err) => {
|
public requestNotification() {
|
||||||
console.error(`Notification permission error`, err);
|
if (window.Notification && Notification.permission !== "granted") {
|
||||||
});
|
return Notification.requestPermission();
|
||||||
} else {
|
} else {
|
||||||
Notification.requestPermission();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
|
|
||||||
* if we are on Safari...
|
|
||||||
*
|
|
||||||
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
|
||||||
*/
|
|
||||||
private checkNotificationPromise(): boolean {
|
|
||||||
try {
|
|
||||||
Notification.requestPermission().then();
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public createNotification(userName: string) {
|
public createNotification(userName: string) {
|
||||||
if (this.focused) {
|
if (document.hasFocus()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (window.Notification && Notification.permission === "granted") {
|
|
||||||
const title = "WorkAdventure";
|
if (this.hasNotification()) {
|
||||||
|
const title = `${userName} wants to discuss with you`;
|
||||||
const options = {
|
const options = {
|
||||||
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`,
|
|
||||||
icon: "/resources/logos/logo-WA-min.png",
|
icon: "/resources/logos/logo-WA-min.png",
|
||||||
image: "/resources/logos/logo-WA-min.png",
|
image: "/resources/logos/logo-WA-min.png",
|
||||||
badge: "/resources/logos/logo-WA-min.png",
|
badge: "/resources/logos/logo-WA-min.png",
|
||||||
};
|
};
|
||||||
new Notification(title, options);
|
new Notification(title, options);
|
||||||
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import type * as SimplePeerNamespace from "simple-peer";
|
import type * as SimplePeerNamespace from "simple-peer";
|
||||||
import { mediaManager } from "./MediaManager";
|
|
||||||
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
|
|
||||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
|
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
|
||||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||||
import { Readable, readable, writable, Writable } from "svelte/store";
|
import { Readable, readable } from "svelte/store";
|
||||||
import { videoFocusStore } from "../Stores/VideoFocusStore";
|
import { videoFocusStore } from "../Stores/VideoFocusStore";
|
||||||
|
import { getIceServersConfig } from "../Components/Video/utils";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||||
|
|
||||||
@ -32,21 +31,9 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
stream: MediaStream | null
|
stream: MediaStream | null
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator,
|
||||||
//reconnectTimer: 10000,
|
|
||||||
config: {
|
config: {
|
||||||
iceServers: [
|
iceServers: getIceServersConfig(user),
|
||||||
{
|
|
||||||
urls: STUN_SERVER.split(","),
|
|
||||||
},
|
|
||||||
TURN_SERVER !== ""
|
|
||||||
? {
|
|
||||||
urls: TURN_SERVER.split(","),
|
|
||||||
username: user.webRtcUser || TURN_USER,
|
|
||||||
credential: user.webRtcPassword || TURN_PASSWORD,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
].filter((value) => value !== undefined),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore }
|
|||||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||||
import { discussionManager } from "./DiscussionManager";
|
import { discussionManager } from "./DiscussionManager";
|
||||||
import { playersStore } from "../Stores/PlayersStore";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
|
import { newChatMessageStore } from "../Stores/ChatStore";
|
||||||
|
|
||||||
export interface UserSimplePeerInterface {
|
export interface UserSimplePeerInterface {
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -155,27 +156,11 @@ export class SimplePeer {
|
|||||||
|
|
||||||
const name = this.getName(user.userId);
|
const name = this.getName(user.userId);
|
||||||
|
|
||||||
discussionManager.removeParticipant(user.userId);
|
|
||||||
|
|
||||||
this.lastWebrtcUserName = user.webRtcUser;
|
this.lastWebrtcUserName = user.webRtcUser;
|
||||||
this.lastWebrtcPassword = user.webRtcPassword;
|
this.lastWebrtcPassword = user.webRtcPassword;
|
||||||
|
|
||||||
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
|
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
|
||||||
|
|
||||||
//permit to send message
|
|
||||||
mediaManager.addSendMessageCallback(user.userId, (message: string) => {
|
|
||||||
peer.write(
|
|
||||||
new Buffer(
|
|
||||||
JSON.stringify({
|
|
||||||
type: MESSAGE_TYPE_MESSAGE,
|
|
||||||
name: this.myName.toUpperCase(),
|
|
||||||
userId: this.userId,
|
|
||||||
message: message,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
peer.toClose = false;
|
peer.toClose = false;
|
||||||
// When a connection is established to a video stream, and if a screen sharing is taking place,
|
// When a connection is established to a video stream, and if a screen sharing is taking place,
|
||||||
// the user sharing screen should also initiate a connection to the remote user!
|
// the user sharing screen should also initiate a connection to the remote user!
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import type * as SimplePeerNamespace from "simple-peer";
|
import type * as SimplePeerNamespace from "simple-peer";
|
||||||
import { mediaManager } from "./MediaManager";
|
import { mediaManager } from "./MediaManager";
|
||||||
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
|
|
||||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||||
import { blackListManager } from "./BlackListManager";
|
import { blackListManager } from "./BlackListManager";
|
||||||
import type { Subscription } from "rxjs";
|
import type { Subscription } from "rxjs";
|
||||||
import type { UserSimplePeerInterface } from "./SimplePeer";
|
import type { UserSimplePeerInterface } from "./SimplePeer";
|
||||||
import { get, readable, Readable } from "svelte/store";
|
import { get, readable, Readable, Unsubscriber } from "svelte/store";
|
||||||
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||||
import { discussionManager } from "./DiscussionManager";
|
|
||||||
import { playersStore } from "../Stores/PlayersStore";
|
import { playersStore } from "../Stores/PlayersStore";
|
||||||
|
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
|
||||||
|
import { getIceServersConfig } from "../Components/Video/utils";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||||
|
|
||||||
@ -34,6 +34,8 @@ export class VideoPeer extends Peer {
|
|||||||
public readonly streamStore: Readable<MediaStream | null>;
|
public readonly streamStore: Readable<MediaStream | null>;
|
||||||
public readonly statusStore: Readable<PeerStatus>;
|
public readonly statusStore: Readable<PeerStatus>;
|
||||||
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
|
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
|
||||||
|
private newMessageunsubscriber: Unsubscriber | null = null;
|
||||||
|
private closing: Boolean = false; //this is used to prevent destroy() from being called twice
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public user: UserSimplePeerInterface,
|
public user: UserSimplePeerInterface,
|
||||||
@ -43,21 +45,9 @@ export class VideoPeer extends Peer {
|
|||||||
localStream: MediaStream | null
|
localStream: MediaStream | null
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator,
|
||||||
//reconnectTimer: 10000,
|
|
||||||
config: {
|
config: {
|
||||||
iceServers: [
|
iceServers: getIceServersConfig(user),
|
||||||
{
|
|
||||||
urls: STUN_SERVER.split(","),
|
|
||||||
},
|
|
||||||
TURN_SERVER !== ""
|
|
||||||
? {
|
|
||||||
urls: TURN_SERVER.split(","),
|
|
||||||
username: user.webRtcUser || TURN_USER,
|
|
||||||
credential: user.webRtcPassword || TURN_PASSWORD,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
].filter((value) => value !== undefined),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -147,6 +137,20 @@ export class VideoPeer extends Peer {
|
|||||||
|
|
||||||
this.on("connect", () => {
|
this.on("connect", () => {
|
||||||
this._connected = true;
|
this._connected = true;
|
||||||
|
chatMessagesStore.addIncomingUser(this.userId);
|
||||||
|
|
||||||
|
this.newMessageunsubscriber = newChatMessageStore.subscribe((newMessage) => {
|
||||||
|
if (!newMessage) return;
|
||||||
|
this.write(
|
||||||
|
new Buffer(
|
||||||
|
JSON.stringify({
|
||||||
|
type: MESSAGE_TYPE_MESSAGE,
|
||||||
|
message: newMessage,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
); //send more data
|
||||||
|
newChatMessageStore.set(null); //This is to prevent a newly created SimplePeer to send an old message a 2nd time. Is there a better way?
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on("data", (chunk: Buffer) => {
|
this.on("data", (chunk: Buffer) => {
|
||||||
@ -164,8 +168,9 @@ export class VideoPeer extends Peer {
|
|||||||
mediaManager.disabledVideoByUserId(this.userId);
|
mediaManager.disabledVideoByUserId(this.userId);
|
||||||
}
|
}
|
||||||
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
|
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
|
||||||
if (!blackListManager.isBlackListed(message.userId)) {
|
if (!blackListManager.isBlackListed(this.userUuid)) {
|
||||||
mediaManager.addNewMessage(message.name, message.message);
|
chatMessagesStore.addExternalMessage(this.userId, message.message);
|
||||||
|
chatVisibilityStore.set(true);
|
||||||
}
|
}
|
||||||
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
|
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
|
||||||
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
|
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
|
||||||
@ -245,18 +250,18 @@ export class VideoPeer extends Peer {
|
|||||||
/**
|
/**
|
||||||
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
* This is triggered twice. Once by the server, and once by a remote client disconnecting
|
||||||
*/
|
*/
|
||||||
public destroy(error?: Error): void {
|
public destroy(): void {
|
||||||
try {
|
try {
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
if (!this.toClose) {
|
if (!this.toClose || this.closing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.closing = true;
|
||||||
this.onBlockSubscribe.unsubscribe();
|
this.onBlockSubscribe.unsubscribe();
|
||||||
this.onUnBlockSubscribe.unsubscribe();
|
this.onUnBlockSubscribe.unsubscribe();
|
||||||
discussionManager.removeParticipant(this.userId);
|
if (this.newMessageunsubscriber) this.newMessageunsubscriber();
|
||||||
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
|
chatMessagesStore.addOutcomingUser(this.userId);
|
||||||
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
|
super.destroy();
|
||||||
super.destroy(error);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("VideoPeer::destroy", err);
|
console.error("VideoPeer::destroy", err);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
@import "~@fontsource/press-start-2p/index.css";
|
@import "~@fontsource/press-start-2p/index.css";
|
||||||
|
|
||||||
*{
|
|
||||||
font-family: PixelFont-7,monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nes-btn {
|
.nes-btn {
|
||||||
font-family: "Press Start 2P";
|
font-family: "Press Start 2P";
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
*{
|
*{
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: Lato;
|
||||||
cursor: url('./images/cursor_normal.png'), auto;
|
cursor: url('./images/cursor_normal.png'), auto;
|
||||||
}
|
}
|
||||||
* a, button, select{
|
* a, button, select{
|
||||||
|
@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|||||||
import sveltePreprocess from "svelte-preprocess";
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||||
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
|
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
|
||||||
import { DISPLAY_TERMS_OF_USE } from "./src/Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
const mode = process.env.NODE_ENV ?? "development";
|
const mode = process.env.NODE_ENV ?? "development";
|
||||||
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
|
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
|
||||||
|
@ -262,10 +262,10 @@
|
|||||||
"@types/mime" "^1"
|
"@types/mime" "^1"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/simple-peer@^9.6.0":
|
"@types/simple-peer@^9.11.1":
|
||||||
version "9.6.3"
|
version "9.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.6.3.tgz#aa118a57e036f4ce2059a7e25367526a4764206d"
|
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.1.tgz#bef6ff1e75178d83438e33aa6a4df2fd98fded1d"
|
||||||
integrity sha512-zrXEBch9tF4NgkZDsGR3c1D0kq99M1bBCjzEyL0PVfEWzCIXrK64TuxRz3XKOx1B0KoEQ9kTs+AhMDuQaHy5RQ==
|
integrity sha512-Pzqbau/WlivSXdRC0He2Wz/ANj2wbi4gzJrtysZz93jvOyI2jo/ibMjUe6AvPllFl/UO6QXT/A0Rcp44bDQB5A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
@ -5008,7 +5008,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||||
|
|
||||||
simple-peer@^9.6.2:
|
simple-peer@^9.11.0:
|
||||||
version "9.11.0"
|
version "9.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571"
|
resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571"
|
||||||
integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg==
|
integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg==
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
WA.room.getCurrentUser().then((user) => {
|
WA.player.getCurrentUser().then((user) => {
|
||||||
console.log('id : ', user.id);
|
console.log('id : ', user.id);
|
||||||
console.log('nickName : ', user.nickName);
|
console.log('nickName : ', user.nickName);
|
||||||
console.log('tags : ', user.tags);
|
console.log('tags : ', user.tags);
|
||||||
|
Loading…
Reference in New Issue
Block a user