Merge pull request #1111 from thecodingmachine/svelteChat

FEATURE: migrated the chat window to svelte
This commit is contained in:
Kharhamel 2021-07-13 11:25:38 +02:00 committed by GitHub
commit 41a1f56bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 562 additions and 305 deletions

View File

@ -17,6 +17,11 @@
- 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
## Version 1.4.3 - 1.4.4 - 1.4.5 ## Version 1.4.3 - 1.4.4 - 1.4.5

View File

@ -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;

View File

@ -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)
//}
} }
} }

View File

@ -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>

BIN
front/dist/static/images/send.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -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>

View 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}>&times</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: 'Whiteney';
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>

View 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}
&gt;&gt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} enter <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.userOutcoming}
&lt;&lt; {#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: 'Whiteney';
}
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>

View File

@ -0,0 +1,57 @@
<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: Whiteney;
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
outline: none;
}
button {
background-color: #42464d;
color: white;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
border: none;
border-left: solid black 1px;
font-size: 16px;
font-family: Whiteney;
}
}
</style>

View 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>

View 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>

View File

@ -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));
} }
} }

View File

@ -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);
} }

View File

@ -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");

View File

@ -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;
} }

View 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();

View File

@ -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;
}); });

View File

@ -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;
} }
); );

View 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);
}

View File

@ -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();
} }
} }

View File

@ -1,16 +1,11 @@
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 { 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;
@ -182,22 +177,8 @@ 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 getNotification() {

View File

@ -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!

View File

@ -5,10 +5,11 @@ 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 { discussionManager } from "./DiscussionManager";
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer"); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -34,6 +35,7 @@ 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;
constructor( constructor(
public user: UserSimplePeerInterface, public user: UserSimplePeerInterface,
@ -147,6 +149,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 +180,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.
@ -253,7 +270,9 @@ export class VideoPeer extends Peer {
} }
this.onBlockSubscribe.unsubscribe(); this.onBlockSubscribe.unsubscribe();
this.onUnBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe();
discussionManager.removeParticipant(this.userId); if (this.newMessageunsubscriber) this.newMessageunsubscriber();
chatMessagesStore.addOutcomingUser(this.userId);
//discussionManager.removeParticipant(this.userId);
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
super.destroy(error); super.destroy(error);

View File

@ -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";
} }

View File

@ -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;