FEATURE: migrated the chat window to svelte
This commit is contained in:
parent
e5f7c62e25
commit
3cfbcc6b02
@ -17,6 +17,11 @@
|
||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||
- Use `WA.room.setTiles(): void` to 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.
|
||||
- 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
|
||||
|
||||
|
@ -15,12 +15,6 @@ import { Admin } from "../Model/Admin";
|
||||
export type ConnectCallback = (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 {
|
||||
private readonly minDistance: number;
|
||||
private readonly groupRadius: number;
|
||||
|
@ -436,10 +436,7 @@ export class SocketManager {
|
||||
const serverToClientMessage1 = new ServerToClientMessage();
|
||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||
|
||||
//if (!user.socket.disconnecting) {
|
||||
user.socket.write(serverToClientMessage1);
|
||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
||||
//}
|
||||
|
||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||
webrtcStartMessage2.setUserid(user.id);
|
||||
@ -453,10 +450,7 @@ export class SocketManager {
|
||||
const serverToClientMessage2 = new ServerToClientMessage();
|
||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||
|
||||
//if (!otherUser.socket.disconnecting) {
|
||||
otherUser.socket.write(serverToClientMessage2);
|
||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
|
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">
|
||||
<!-- Create the editor container -->
|
||||
<div id="game" class="game">
|
||||
<div id="svelte-overlay">
|
||||
</div>
|
||||
<div id="svelte-overlay"></div>
|
||||
<div id="game-overlay" class="game-overlay">
|
||||
<div id="main-section" class="main-section">
|
||||
</div>
|
||||
|
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 |
@ -10,12 +10,14 @@
|
||||
import {errorStore} from "../Stores/ErrorStore";
|
||||
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
||||
import LoginScene from "./Login/LoginScene.svelte";
|
||||
import Chat from "./Chat/Chat.svelte";
|
||||
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
||||
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
||||
import VisitCard from "./VisitCard/VisitCard.svelte";
|
||||
import {requestVisitCardsStore} from "../Stores/GameStore";
|
||||
|
||||
import type {Game} from "../Phaser/Game/Game";
|
||||
import {chatVisibilityStore} from "../Stores/ChatStore";
|
||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
@ -61,14 +63,6 @@
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
{#if $menuIconVisible}
|
||||
<div>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
{/if}
|
||||
-->
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<VideoOverlay></VideoOverlay>
|
||||
@ -94,4 +88,7 @@
|
||||
<ErrorDialog></ErrorDialog>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $chatVisibilityStore}
|
||||
<Chat></Chat>
|
||||
{/if}
|
||||
</div>
|
||||
|
97
front/src/Components/Chat/Chat.svelte
Normal file
97
front/src/Components/Chat/Chat.svelte
Normal file
@ -0,0 +1,97 @@
|
||||
<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 {onMount} from "svelte";
|
||||
|
||||
let listDom: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
listDom.addEventListener('onscroll', function(e: Event) {
|
||||
console.log(e);
|
||||
// Active list item is top-most fully-visible item
|
||||
//const visibleListItems = Array.from(document.getElementsByClassName('list-item')).map(inView.is);
|
||||
// Array.indexOf() will give us the first one in list, so the current active item
|
||||
//const topMostVisible = visibleListItems.indexOf(true);
|
||||
});
|
||||
})
|
||||
|
||||
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">
|
||||
<h3>Here is your chat history <button on:click={closeChat}>❌</button></h3>
|
||||
|
||||
</section>
|
||||
<section class="messagesList">
|
||||
<ul bind:this={listDom}>
|
||||
{#each $chatMessagesStore as message}
|
||||
<li><ChatElement message={message}></ChatElement></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="messageForm">
|
||||
<ChatMessageForm></ChatMessageForm>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<style lang="scss">
|
||||
h3 {
|
||||
font-family: 'Whiteney';
|
||||
}
|
||||
aside.chatWindow {
|
||||
z-index:100;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width:30vw;
|
||||
min-width: 350px;
|
||||
background: #051f33;
|
||||
color: whitesmoke;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-bottom-right-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
|
||||
h3 {
|
||||
background-color: #5f5f5f;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.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>
|
74
front/src/Components/Chat/ChatElement.svelte
Normal file
74
front/src/Components/Chat/ChatElement.svelte
Normal file
@ -0,0 +1,74 @@
|
||||
<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;
|
||||
|
||||
$: 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'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="chatElement">
|
||||
<div class="messagePart">
|
||||
{#if message.type === ChatMessageTypes.userIncoming}
|
||||
➡️: {#each targets as target}<ChatPlayerName player={target}></ChatPlayerName>{/each} ({renderDate(message.date)})
|
||||
{:else if message.type === ChatMessageTypes.userOutcoming}
|
||||
⬅️: {#each targets as target}<ChatPlayerName player={target}></ChatPlayerName>{/each} ({renderDate(message.date)})
|
||||
{:else if message.type === ChatMessageTypes.me}
|
||||
<h4>Me: ({renderDate(message.date)})</h4>
|
||||
{#each texts as text}
|
||||
<div><p class="my-text">{@html urlifyText(text)}</p></div>
|
||||
{/each}
|
||||
{:else}
|
||||
<h4><ChatPlayerName player={author}></ChatPlayerName>: ({renderDate(message.date)})</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%;
|
||||
|
||||
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>
|
55
front/src/Components/Chat/ChatMessageForm.svelte
Normal file
55
front/src/Components/Chat/ChatMessageForm.svelte
Normal file
@ -0,0 +1,55 @@
|
||||
<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="Type here" 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;
|
||||
}
|
||||
|
||||
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>
|
37
front/src/Components/Chat/ChatPlayerName.svelte
Normal file
37
front/src/Components/Chat/ChatPlayerName.svelte
Normal file
@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||
|
||||
export let player:PlayerInterface;
|
||||
let showMenu: boolean = false;
|
||||
|
||||
function openVisitCard() {
|
||||
if (player.visitCardUrl) {
|
||||
requestVisitCardsStore.set(player.visitCardUrl);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={() => showMenu = !showMenu}>
|
||||
{player.name}
|
||||
</span>
|
||||
|
||||
{#if showMenu}
|
||||
<ul class="selectMenu">
|
||||
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li>
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.chatPlayerName:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
ul.selectMenu {
|
||||
background-color: whitesmoke;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
import {discussionManager} from "../../WebRtc/DiscussionManager";
|
||||
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 {
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y, openChatIconName, 3);
|
||||
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
|
||||
this.setScrollFactor(0, 0);
|
||||
this.setOrigin(0, 1);
|
||||
this.setInteractive();
|
||||
this.setVisible(false);
|
||||
//this.setVisible(false);
|
||||
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
|
||||
) => {
|
||||
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
|
||||
console.log("count", loadPlugin.listenerCount("loaderror"));
|
||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||
return res(playerResourceDescriptor);
|
||||
}
|
||||
|
@ -692,12 +692,12 @@ export class GameScene extends DirtyScene {
|
||||
const self = this;
|
||||
this.simplePeer.registerPeerConnectionListener({
|
||||
onConnect(peer) {
|
||||
self.openChatIcon.setVisible(true);
|
||||
//self.openChatIcon.setVisible(true);
|
||||
audioManager.decreaseVolume();
|
||||
},
|
||||
onDisconnect(userId: number) {
|
||||
if (self.simplePeer.getNbConnections() === 0) {
|
||||
self.openChatIcon.setVisible(false);
|
||||
//self.openChatIcon.setVisible(false);
|
||||
audioManager.restoreVolume();
|
||||
}
|
||||
},
|
||||
|
@ -7,4 +7,5 @@ export interface PlayerInterface {
|
||||
visitCardUrl: string | null;
|
||||
companion: string | null;
|
||||
userUuid: string;
|
||||
color?: string;
|
||||
}
|
||||
|
102
front/src/Stores/ChatStore.ts
Normal file
102
front/src/Stores/ChatStore.ts
Normal file
@ -0,0 +1,102 @@
|
||||
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();
|
@ -1,6 +1,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
|
||||
import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||
import { getRandomColor } from "../WebRtc/ColorGenerator";
|
||||
|
||||
/**
|
||||
* A store that contains the list of players currently known.
|
||||
@ -24,6 +25,7 @@ function createPlayersStore() {
|
||||
visitCardUrl: message.visitCardUrl,
|
||||
companion: message.companion,
|
||||
userUuid: message.userUuid,
|
||||
color: getRandomColor(),
|
||||
});
|
||||
return users;
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { derived } from "svelte/store";
|
||||
import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
|
||||
import { chatInputFocusStore } from "./ChatStore";
|
||||
|
||||
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
|
||||
export const enableUserInputsStore = derived(
|
||||
consoleGlobalMessageManagerFocusStore,
|
||||
($consoleGlobalMessageManagerFocusStore) => {
|
||||
return !$consoleGlobalMessageManagerFocusStore;
|
||||
[consoleGlobalMessageManagerFocusStore, chatInputFocusStore],
|
||||
([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => {
|
||||
return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore;
|
||||
}
|
||||
);
|
48
front/src/WebRtc/ColorGenerator.ts
Normal file
48
front/src/WebRtc/ColorGenerator.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export function getRandomColor(): string {
|
||||
return hsv_to_rgb(Math.random(), 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 { showReportScreenStore } from "../Stores/ShowReportScreenStore";
|
||||
|
||||
export type SendMessageCallback = (message: string) => void;
|
||||
import { chatMessagesStore, chatVisibilityStore } from "../Stores/ChatStore";
|
||||
|
||||
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() {
|
||||
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
|
||||
this.createDiscussPart(""); //todo: why do we always use empty string?
|
||||
|
||||
iframeListener.chatStream.subscribe((chatEvent) => {
|
||||
this.addMessage(chatEvent.author, chatEvent.message, false);
|
||||
this.showDiscussion();
|
||||
chatMessagesStore.addExternalMessage(parseInt(chatEvent.author), chatEvent.message);
|
||||
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,11 @@
|
||||
import { DivImportance, layoutManager } from "./LayoutManager";
|
||||
import { layoutManager } from "./LayoutManager";
|
||||
import { HtmlUtils } from "./HtmlUtils";
|
||||
import { discussionManager, SendMessageCallback } from "./DiscussionManager";
|
||||
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 { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
||||
|
||||
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
|
||||
export type StartScreenSharingCallback = (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) {
|
||||
this.userInputManager = userInputManager;
|
||||
discussionManager.setUserInputManager(userInputManager);
|
||||
}
|
||||
|
||||
public getNotification() {
|
||||
|
@ -12,6 +12,7 @@ import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore }
|
||||
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
|
||||
import { discussionManager } from "./DiscussionManager";
|
||||
import { playersStore } from "../Stores/PlayersStore";
|
||||
import { newChatMessageStore } from "../Stores/ChatStore";
|
||||
|
||||
export interface UserSimplePeerInterface {
|
||||
userId: number;
|
||||
@ -155,27 +156,11 @@ export class SimplePeer {
|
||||
|
||||
const name = this.getName(user.userId);
|
||||
|
||||
discussionManager.removeParticipant(user.userId);
|
||||
|
||||
this.lastWebrtcUserName = user.webRtcUser;
|
||||
this.lastWebrtcPassword = user.webRtcPassword;
|
||||
|
||||
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;
|
||||
// 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!
|
||||
|
@ -5,10 +5,11 @@ import type { RoomConnection } from "../Connexion/RoomConnection";
|
||||
import { blackListManager } from "./BlackListManager";
|
||||
import type { Subscription } from "rxjs";
|
||||
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 { discussionManager } from "./DiscussionManager";
|
||||
import { playersStore } from "../Stores/PlayersStore";
|
||||
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
|
||||
|
||||
@ -34,6 +35,7 @@ export class VideoPeer extends Peer {
|
||||
public readonly streamStore: Readable<MediaStream | null>;
|
||||
public readonly statusStore: Readable<PeerStatus>;
|
||||
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
|
||||
private newMessageunsubscriber: Unsubscriber | null = null;
|
||||
|
||||
constructor(
|
||||
public user: UserSimplePeerInterface,
|
||||
@ -147,6 +149,20 @@ export class VideoPeer extends Peer {
|
||||
|
||||
this.on("connect", () => {
|
||||
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) => {
|
||||
@ -164,8 +180,9 @@ export class VideoPeer extends Peer {
|
||||
mediaManager.disabledVideoByUserId(this.userId);
|
||||
}
|
||||
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
|
||||
if (!blackListManager.isBlackListed(message.userId)) {
|
||||
mediaManager.addNewMessage(message.name, message.message);
|
||||
if (!blackListManager.isBlackListed(this.userUuid)) {
|
||||
chatMessagesStore.addExternalMessage(this.userId, message.message);
|
||||
chatVisibilityStore.set(true);
|
||||
}
|
||||
} 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.
|
||||
@ -253,7 +270,9 @@ export class VideoPeer extends Peer {
|
||||
}
|
||||
this.onBlockSubscribe.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"
|
||||
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
|
||||
super.destroy(error);
|
||||
|
@ -1,9 +1,5 @@
|
||||
@import "~@fontsource/press-start-2p/index.css";
|
||||
|
||||
*{
|
||||
font-family: PixelFont-7,monospace;
|
||||
}
|
||||
|
||||
.nes-btn {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-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 buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
|
||||
|
Loading…
Reference in New Issue
Block a user