diff --git a/ChangeLog.md b/ChangeLog.md
index e67fc928..119036c9 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,4 +1,13 @@
# Changelog:
+* **24.09.20**
+ - Improved the server tab menu
+ - Better scroll handling
+ - Automatic update on server state changes
+ - Added an audio playback indicator
+ - Rendering the server icon
+ - Changing the favicon according to the clients status
+ - Aborting all replaying audio streams when client mutes himself
+
* **17.09.20**
- Added a settings registry and some minor bug fixing
diff --git a/loader/html/index.html.ejs b/loader/html/index.html.ejs
index 326ea623..f1025383 100644
--- a/loader/html/index.html.ejs
+++ b/loader/html/index.html.ejs
@@ -27,7 +27,7 @@ var initial_css;
<% } else { %>
diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts
index 2ddbf0b3..346cc3bd 100644
--- a/shared/js/ConnectionHandler.ts
+++ b/shared/js/ConnectionHandler.ts
@@ -1074,6 +1074,7 @@ export class ConnectionHandler {
this.event_registry.fire("notify_state_updated", { state: "speaker" });
if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
this.update_voice_status();
+ this.serverConnection.getVoiceConnection().stopAllVoiceReplays();
}
toggleSpeakerMuted() { this.setSpeakerMuted(!this.isSpeakerMuted()); }
diff --git a/shared/js/ConnectionManager.ts b/shared/js/ConnectionManager.ts
index 944febd5..9485f301 100644
--- a/shared/js/ConnectionManager.ts
+++ b/shared/js/ConnectionManager.ts
@@ -2,12 +2,11 @@ import {ConnectionHandler, DisconnectReason} from "./ConnectionHandler";
import {Settings, settings} from "./settings";
import {Registry} from "./events";
import * as top_menu from "./ui/frames/MenuBar";
+import * as loader from "tc-loader";
+import {Stage} from "tc-loader";
export let server_connections: ConnectionManager;
-export function initializeServerConnections() {
- if(server_connections) throw tr("Connection manager has already been initialized");
- server_connections = new ConnectionManager($("#connection-handlers"));
-}
+
export class ConnectionManager {
private readonly event_registry: Registry
;
private connection_handlers: ConnectionHandler[] = [];
@@ -63,7 +62,7 @@ export class ConnectionManager {
this._tag.toggleClass("shown", this.connection_handlers.length > 1);
this._update_scroll();
- this.event_registry.fire("notify_handler_created", { handler: handler });
+ this.event_registry.fire("notify_handler_created", { handler: handler, handlerId: handler.handlerId });
return handler;
}
@@ -86,7 +85,7 @@ export class ConnectionManager {
if(handler === this.active_handler)
this.set_active_connection_(this.connection_handlers[0]);
- this.event_registry.fire("notify_handler_deleted", { handler: handler });
+ this.event_registry.fire("notify_handler_deleted", { handler: handler, handlerId: handler.handlerId });
/* destroy all elements */
handler.destroy();
@@ -118,8 +117,11 @@ export class ConnectionManager {
const old_handler = this.active_handler;
this.active_handler = handler;
this.event_registry.fire("notify_active_handler_changed", {
- old_handler: old_handler,
- new_handler: handler
+ oldHandler: old_handler,
+ newHandler: handler,
+
+ oldHandlerId: old_handler?.handlerId,
+ newHandlerId: handler?.handlerId
});
old_handler?.events().fire("notify_visibility_changed", { visible: false });
handler?.events().fire("notify_visibility_changed", { visible: true });
@@ -173,18 +175,31 @@ export class ConnectionManager {
export interface ConnectionManagerEvents {
notify_handler_created: {
+ handlerId: string,
handler: ConnectionHandler
},
/* This will also trigger when a connection gets deleted. So if you're just interested to connect event handler to the active connection,
unregister them from the old handler and register them for the new handler every time */
notify_active_handler_changed: {
- old_handler: ConnectionHandler | undefined,
- new_handler: ConnectionHandler | undefined
+ oldHandler: ConnectionHandler | undefined,
+ newHandler: ConnectionHandler | undefined,
+
+ oldHandlerId: string | undefined,
+ newHandlerId: string | undefined
},
/* Will never fire on an active connection handler! */
notify_handler_deleted: {
+ handlerId: string,
handler: ConnectionHandler
}
-}
\ No newline at end of file
+}
+
+loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
+ name: "server manager init",
+ function: async () => {
+ server_connections = new ConnectionManager($("#connection-handlers"));
+ },
+ priority: 80
+});
diff --git a/shared/js/connection/DummyVoiceConnection.ts b/shared/js/connection/DummyVoiceConnection.ts
index 202e9458..888f51c7 100644
--- a/shared/js/connection/DummyVoiceConnection.ts
+++ b/shared/js/connection/DummyVoiceConnection.ts
@@ -141,4 +141,10 @@ export class DummyVoiceConnection extends AbstractVoiceConnection {
getFailedMessage(): string {
return "";
}
+
+ isReplayingVoice(): boolean {
+ return false;
+ }
+
+ stopAllVoiceReplays() { }
}
\ No newline at end of file
diff --git a/shared/js/connection/VoiceConnection.ts b/shared/js/connection/VoiceConnection.ts
index 03127d2b..27cffc82 100644
--- a/shared/js/connection/VoiceConnection.ts
+++ b/shared/js/connection/VoiceConnection.ts
@@ -32,6 +32,10 @@ export interface VoiceConnectionEvents {
},
"notify_whisper_destroyed": {
session: WhisperSession
+ },
+
+ "notify_voice_replay_state_change": {
+ replaying: boolean
}
}
@@ -72,6 +76,9 @@ export abstract class AbstractVoiceConnection {
abstract getEncoderCodec() : number;
abstract setEncoderCodec(codec: number);
+ abstract stopAllVoiceReplays();
+ abstract isReplayingVoice() : boolean;
+
/* the whisper API */
abstract getWhisperSessions() : WhisperSession[];
abstract dropWhisperSession(session: WhisperSession);
diff --git a/shared/js/events.ts b/shared/js/events.ts
index 214e450d..189b01ed 100644
--- a/shared/js/events.ts
+++ b/shared/js/events.ts
@@ -114,7 +114,13 @@ export class Registry(event: T, handler: (event?: Events[T] & Event) => void, condition?: boolean) {
+ /**
+ * @param event
+ * @param handler
+ * @param condition
+ * @param reactEffectDependencies
+ */
+ reactUse(event: T, handler: (event?: Events[T] & Event) => void, condition?: boolean, reactEffectDependencies?: any[]) {
if(typeof condition === "boolean" && !condition) {
useEffect(() => {});
return;
@@ -123,7 +129,7 @@ export class Registry {
handlers.push(handler);
return () => handlers.remove(handler);
- });
+ }, reactEffectDependencies);
}
connectAll(target: EventReceiver) {
diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts
index 835aee7c..0dca2d7f 100644
--- a/shared/js/events/ClientGlobalControlHandler.ts
+++ b/shared/js/events/ClientGlobalControlHandler.ts
@@ -46,7 +46,7 @@ export function load_default_states(event_registry: Registry) {
let current_connection_handler: ConnectionHandler | undefined;
- server_connections.events().on("notify_active_handler_changed", event => current_connection_handler = event.new_handler);
+ server_connections.events().on("notify_active_handler_changed", event => current_connection_handler = event.newHandler);
//initialize_sounds(event_registry);
event_registry.on("action_open_window", event => {
diff --git a/shared/js/main.tsx b/shared/js/main.tsx
index 71195787..9c49c903 100644
--- a/shared/js/main.tsx
+++ b/shared/js/main.tsx
@@ -1,4 +1,5 @@
import * as loader from "tc-loader";
+import {Stage} from "tc-loader";
import {settings, Settings} from "tc-shared/settings";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
@@ -6,7 +7,7 @@ import * as bipc from "./ipc/BrowserIPC";
import * as sound from "./sound/Sounds";
import * as i18n from "./i18n/localize";
import {tra} from "./i18n/localize";
-import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
+import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {createInfoModal} from "tc-shared/ui/elements/Modal";
import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity";
@@ -42,10 +43,10 @@ import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
import "./video-viewer/Controller";
import "./profiles/ConnectionProfile";
import "./update/UpdaterWeb";
-import ContextMenuEvent = JQuery.ContextMenuEvent;
import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/ConnectionProfile";
-import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller";
-import {initializeServerConnections, server_connections} from "tc-shared/ConnectionManager";
+import {server_connections} from "tc-shared/ConnectionManager";
+import {initializeConnectionUIList} from "tc-shared/ui/frames/connection-handler-list/Controller";
+import ContextMenuEvent = JQuery.ContextMenuEvent;
let preventWelcomeUI = false;
async function initialize() {
@@ -61,20 +62,8 @@ async function initialize() {
}
async function initialize_app() {
- try { //Initialize main template
- const main = $("#tmpl_main").renderTag({
- multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
- app_version: __build.version
- }).dividerfy();
+ initializeConnectionUIList();
- $("body").append(main);
- } catch(error) {
- log.error(LogCategory.GENERAL, error);
- loader.critical_error(tr("Failed to setup main page!"));
- return;
- }
-
- initializeServerConnections();
global_ev_handler.initialize(global_client_actions);
{
const bar = (
@@ -83,6 +72,7 @@ async function initialize_app() {
ReactDOM.render(bar, $(".container-control-bar")[0]);
}
+
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "settings init",
@@ -201,6 +191,16 @@ function main() {
const initial_handler = server_connections.spawn_server_connection();
initial_handler.acquireInputHardware().then(() => {});
server_connections.set_active_connection(initial_handler);
+
+ /* Just a test */
+ {
+ server_connections.spawn_server_connection();
+ server_connections.spawn_server_connection();
+ server_connections.spawn_server_connection();
+ server_connections.spawn_server_connection();
+ server_connections.spawn_server_connection();
+ }
+
/** Setup the XF forum identity **/
fidentity.update_forum();
keycontrol.initialize();
@@ -476,7 +476,7 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
return;
}
},
- priority: 100
+ priority: 110
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
@@ -511,4 +511,23 @@ loader.register_task(loader.Stage.LOADED, {
}
},
priority: 20
+});
+
+loader.register_task(Stage.JAVASCRIPT_INITIALIZING,{
+ name: "app init",
+ function: async () => {
+ try { //Initialize main template
+ const main = $("#tmpl_main").renderTag({
+ multi_session: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION),
+ app_version: __build.version
+ }).dividerfy();
+
+ $("body").append(main);
+ } catch(error) {
+ log.error(LogCategory.GENERAL, error);
+ loader.critical_error(tr("Failed to setup main page!"));
+ return;
+ }
+ },
+ priority: 100
});
\ No newline at end of file
diff --git a/shared/js/tree/Client.ts b/shared/js/tree/Client.ts
index 593be195..197cade4 100644
--- a/shared/js/tree/Client.ts
+++ b/shared/js/tree/Client.ts
@@ -264,6 +264,38 @@ export class ClientEntry extends ChannelTreeEntry {
return this._properties;
}
+ getStatusIcon() : ClientIcon {
+ if (this.properties.client_type_exact == ClientType.CLIENT_QUERY) {
+ return ClientIcon.ServerQuery;
+ } else if (this.properties.client_away) {
+ return ClientIcon.Away;
+ } else if (!this.getVoiceClient() && !(this instanceof LocalClientEntry)) {
+ return ClientIcon.InputMutedLocal;
+ } else if (!this.properties.client_output_hardware) {
+ return ClientIcon.HardwareOutputMuted;
+ } else if (this.properties.client_output_muted) {
+ return ClientIcon.OutputMuted;
+ } else if (!this.properties.client_input_hardware) {
+ return ClientIcon.HardwareInputMuted;
+ } else if (this.properties.client_input_muted) {
+ return ClientIcon.InputMuted;
+ } else {
+ if (this.isSpeaking()) {
+ if (this.properties.client_is_channel_commander) {
+ return ClientIcon.PlayerCommanderOn;
+ } else {
+ return ClientIcon.PlayerOn;
+ }
+ } else {
+ if (this.properties.client_is_channel_commander) {
+ return ClientIcon.PlayerCommanderOff;
+ } else {
+ return ClientIcon.PlayerOff;
+ }
+ }
+ }
+ }
+
currentChannel() : ChannelEntry { return this._channel; }
clientNickName(){ return this.properties.client_nickname; }
clientUid(){ return this.properties.client_unique_identifier; }
diff --git a/shared/js/ui/frames/ConnectionList.scss b/shared/js/ui/frames/ConnectionList.scss
deleted file mode 100644
index e69de29b..00000000
diff --git a/shared/js/ui/frames/ConnectionList.tsx b/shared/js/ui/frames/ConnectionList.tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/shared/js/ui/frames/connection-handler-list/Controller.ts b/shared/js/ui/frames/connection-handler-list/Controller.ts
new file mode 100644
index 00000000..88b3cf5d
--- /dev/null
+++ b/shared/js/ui/frames/connection-handler-list/Controller.ts
@@ -0,0 +1,125 @@
+import {Registry} from "tc-shared/events";
+import {ConnectionListUIEvents, HandlerConnectionState} from "tc-shared/ui/frames/connection-handler-list/Definitions";
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import {ConnectionHandlerList} from "tc-shared/ui/frames/connection-handler-list/Renderer";
+import {server_connections} from "tc-shared/ConnectionManager";
+import {LogCategory, logWarn} from "tc-shared/log";
+import {ConnectionState} from "tc-shared/ConnectionHandler";
+import {LocalIcon} from "tc-shared/file/Icons";
+
+export function initializeConnectionUIList() {
+ const container = document.getElementById("connection-handler-list");
+ const events = new Registry();
+ events.enableDebug("Handler-List");
+ initializeController(events);
+
+ ReactDOM.render(React.createElement(ConnectionHandlerList, { events: events }), container);
+}
+
+function initializeController(events: Registry) {
+ let registeredHandlerEvents: {[key: string]:(() => void)[]} = {};
+
+ events.on("notify_destroy", () => {
+ Object.keys(registeredHandlerEvents).forEach(handlerId => registeredHandlerEvents[handlerId].forEach(callback => callback()));
+ registeredHandlerEvents = {};
+ });
+
+ events.on("query_handler_list", () => {
+ events.fire_async("notify_handler_list", { handlerIds: server_connections.all_connections().map(e => e.handlerId), activeHandlerId: server_connections.active_connection()?.handlerId });
+ });
+ events.on("notify_destroy", server_connections.events().on("notify_handler_created", event => {
+ let listeners = [];
+
+ const handlerId = event.handlerId;
+ listeners.push(event.handler.events().on("notify_connection_state_changed", () => events.fire_async("query_handler_status", { handlerId: handlerId })));
+
+ /* register to icon and name change updates */
+ listeners.push(event.handler.channelTree.server.events.on("notify_properties_updated", event => {
+ if("virtualserver_name" in event.updated_properties || "virtualserver_icon_id" in event.updated_properties) {
+ events.fire_async("query_handler_status", { handlerId: handlerId });
+ }
+ }));
+
+ /* register to voice playback change events */
+ listeners.push(event.handler.getServerConnection().getVoiceConnection().events.on("notify_voice_replay_state_change", () => {
+ events.fire_async("query_handler_status", { handlerId: handlerId });
+ }));
+
+ registeredHandlerEvents[event.handlerId] = listeners;
+
+ events.fire_async("query_handler_list");
+ }));
+ events.on("notify_destroy", server_connections.events().on("notify_handler_deleted", event => {
+ (registeredHandlerEvents[event.handlerId] || []).forEach(callback => callback());
+ delete registeredHandlerEvents[event.handlerId];
+
+ events.fire_async("query_handler_list");
+ }));
+
+
+ events.on("action_set_active_handler", event => {
+ const handler = server_connections.findConnection(event.handlerId);
+ if(!handler) {
+ logWarn(LogCategory.CLIENT, tr("Tried to activate an invalid server connection handler with id %s"), event.handlerId);
+ return;
+ }
+
+ server_connections.set_active_connection(handler);
+ });
+ events.on("notify_destroy", server_connections.events().on("notify_active_handler_changed", event => {
+ events.fire_async("notify_active_handler", { handlerId: event.newHandlerId });
+ }));
+
+ events.on("action_destroy_handler", event => {
+ const handler = server_connections.findConnection(event.handlerId);
+ if(!handler) {
+ logWarn(LogCategory.CLIENT, tr("Tried to destroy an invalid server connection handler with id %s"), event.handlerId);
+ return;
+ }
+
+ server_connections.destroy_server_connection(handler);
+ });
+
+
+ events.on("query_handler_status", event => {
+ const handler = server_connections.findConnection(event.handlerId);
+ if(!handler) {
+ logWarn(LogCategory.CLIENT, tr("Tried to query a status for an invalid server connection handler with id %s"), event.handlerId);
+ return;
+ }
+
+ let state: HandlerConnectionState;
+ switch (handler.connection_state) {
+ case ConnectionState.CONNECTED:
+ state = "connected";
+ break;
+
+ case ConnectionState.AUTHENTICATING:
+ case ConnectionState.CONNECTING:
+ case ConnectionState.INITIALISING:
+ state = "connecting";
+ break;
+
+ default:
+ state = "disconnected";
+ break;
+ }
+
+ let icon: LocalIcon | undefined;
+ let iconId = handler.channelTree.server.properties.virtualserver_icon_id;
+ if(iconId !== 0) {
+ icon = handler.fileManager.icons.load_icon(handler.channelTree.server.properties.virtualserver_icon_id);
+ }
+
+ events.fire_async("notify_handler_status", {
+ handlerId: event.handlerId,
+ status: {
+ handlerName: handler.channelTree.server.properties.virtualserver_name,
+ connectionState: state,
+ voiceReplaying: handler.getServerConnection().getVoiceConnection().isReplayingVoice(),
+ serverIcon: icon
+ }
+ });
+ });
+}
\ No newline at end of file
diff --git a/shared/js/ui/frames/connection-handler-list/Definitions.ts b/shared/js/ui/frames/connection-handler-list/Definitions.ts
new file mode 100644
index 00000000..6bade959
--- /dev/null
+++ b/shared/js/ui/frames/connection-handler-list/Definitions.ts
@@ -0,0 +1,36 @@
+import {LocalIcon} from "tc-shared/file/Icons";
+
+export type HandlerConnectionState = "disconnected" | "connecting" | "connected";
+
+export type HandlerStatus = {
+ connectionState: HandlerConnectionState,
+ handlerName: string,
+ voiceReplaying: boolean,
+ serverIcon: LocalIcon | undefined
+}
+
+export interface ConnectionListUIEvents {
+ action_set_active_handler: { handlerId: string },
+ action_destroy_handler: { handlerId: string },
+ action_scroll: { direction: "left" | "right" }
+
+ query_handler_status: { handlerId: string },
+ query_handler_list: {},
+
+ notify_handler_list: {
+ handlerIds: string[],
+ activeHandlerId: string | undefined
+ },
+ notify_active_handler: {
+ handlerId: string
+ },
+ notify_handler_status: {
+ handlerId: string,
+ status: HandlerStatus
+ },
+ notify_scroll_status: {
+ left: boolean,
+ right: boolean
+ },
+ notify_destroy: {}
+}
\ No newline at end of file
diff --git a/shared/js/ui/frames/connection-handler-list/Renderer.scss b/shared/js/ui/frames/connection-handler-list/Renderer.scss
new file mode 100644
index 00000000..a346f650
--- /dev/null
+++ b/shared/js/ui/frames/connection-handler-list/Renderer.scss
@@ -0,0 +1,203 @@
+@import "../../../../css/static/properties";
+@import "../../../../css/static/mixin";
+
+.container {
+ $animation_length: .25s;
+
+ max-width: 100%;
+
+ margin-top: 0;
+ height: 0;
+
+ transition: all $animation_length ease-in-out;
+ &.shown {
+ margin-top: -4px;
+ height: 24px;
+
+ transition: all $animation_length ease-in-out;
+ }
+
+ background-color: transparent;
+
+ @include user-select(none);
+
+ position: relative;
+
+ .handlerList {
+ height: 100%;
+ width: fit-content;
+
+ display: flex;
+ flex-direction: row;
+ justify-content: left;
+
+ overflow-x: auto;
+ overflow-y: visible;
+
+ max-width: 100%;
+ scroll-behavior: smooth;
+
+ .handler {
+ padding-top: 4px;
+ position: relative;
+
+ flex-grow: 0;
+ flex-shrink: 0;
+
+ cursor: pointer;
+ display: inline-flex;
+
+ padding-left: 5px;
+ padding-right: 5px;
+
+ height: 24px;
+ overflow: hidden;
+
+ border-bottom: 1px solid transparent;
+ @include transition(all ease-in-out $button_hover_animation_time);
+
+ .icon {
+ width: 16px;
+ height: 16px;
+
+ align-self: center;
+ margin-right: 5px;
+ }
+
+ .name {
+ color: #a8a8a8;
+
+ align-self: center;
+ margin-right: 20px;
+
+ position: relative;
+
+ overflow: visible;
+ text-overflow: clip;
+ white-space: nowrap;
+ }
+
+ .buttonClose {
+ width: 16px;
+ height: 16px;
+
+ position: absolute;
+ right: 5px;
+
+ align-self: center;
+
+ &:hover {
+ background-color: #212121;
+ }
+ }
+
+ &.cutoffName {
+ .name {
+ max-width: 15em;
+ margin-right: -5px; /* 5px padding which have to be overcommed */
+
+ &:before {
+ content: '';
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ background: linear-gradient(to right, transparent calc(100% - 50px), #1e1e1e calc(100% - 25px));
+ }
+ }
+ }
+
+ &:hover {
+ background-color: #242425;
+
+ &.cutoffName .name:before {
+ background: linear-gradient(to right, transparent calc(100% - 50px), #242425 calc(100% - 25px));
+ }
+ }
+
+ &.active {
+ border-bottom-color: #0d9cfd;
+ background-color: #2d2f32;
+
+ &.cutoffName .name:before {
+ background: linear-gradient(to right, transparent calc(100% - 50px), #2d2f32 calc(100% - 25px));
+ }
+ }
+
+ &.audioPlayback {
+ border-bottom-color: #68c1fd;
+ }
+ }
+
+ .scrollSpacer {
+ display: none;
+
+ width: calc(2em + 8px);
+ height: 1px;
+
+ flex-shrink: 0;
+ flex-grow: 0;
+ }
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ .containerScroll {
+ margin-top: 5px;
+ position: absolute;
+
+ top: 0;
+ right: 0;
+ bottom: 0;
+
+ display: none;
+ flex-direction: row;
+
+ &.shown {
+ display: flex;
+ }
+
+ .buttonScroll {
+ cursor: pointer;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+
+ border: 1px solid;
+ @include hex-rgba(border-color, #2222223b);
+
+ border-radius: 2px;
+ background: #e7e7e7;
+ padding-left: 2px;
+ padding-right: 2px;
+
+ &:hover {
+ background: #eeeeee;
+ }
+
+ &.disabled {
+ background: #9e9e9e;
+ &:hover {
+ background: #9e9e9e;
+ }
+ }
+ }
+ }
+
+ &.scrollShown {
+ /*
+ .connection-handlers {
+ width: calc(100% - 45px);
+ }
+ */
+
+ .handlerList .scrollSpacer {
+ display: block;
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/js/ui/frames/connection-handler-list/Renderer.tsx b/shared/js/ui/frames/connection-handler-list/Renderer.tsx
new file mode 100644
index 00000000..ac40c194
--- /dev/null
+++ b/shared/js/ui/frames/connection-handler-list/Renderer.tsx
@@ -0,0 +1,243 @@
+import {Registry} from "tc-shared/events";
+import {ConnectionListUIEvents, HandlerStatus} from "tc-shared/ui/frames/connection-handler-list/Definitions";
+import * as React from "react";
+import {useContext, useEffect, useRef, useState} from "react";
+import {IconRenderer, LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
+import {ClientIcon} from "svg-sprites/client-icons";
+import {Translatable} from "tc-shared/ui/react-elements/i18n";
+import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
+import ResizeObserver from 'resize-observer-polyfill';
+import {LogCategory, logWarn} from "tc-shared/log";
+import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
+
+const cssStyle = require("./Renderer.scss");
+const Events = React.createContext>(undefined);
+
+
+const ConnectionHandler = (props: { handlerId: string, active: boolean }) => {
+ const events = useContext(Events);
+
+ const [ status, setStatus ] = useState(() => {
+ events.fire_async("query_handler_status", { handlerId: props.handlerId });
+ return "loading";
+ });
+
+ events.reactUse("notify_handler_status", event => {
+ if(event.handlerId !== props.handlerId) {
+ return;
+ }
+
+ setStatus(event.status);
+ });
+
+ let displayedName;
+ let cutoffName = false;
+ let voiceReplaying = false;
+ let icon = ;
+ if(status === "loading") {
+ displayedName = tr("loading status");
+ } else {
+ switch (status.connectionState) {
+ case "connected":
+ cutoffName = status.handlerName.length > 30;
+ voiceReplaying = status.voiceReplaying;
+ displayedName = {status.handlerName} ;
+ if(status.serverIcon) {
+ icon = ;
+ }
+ break;
+
+ case "connecting":
+ displayedName = Connecting to server ;
+ break;
+
+ case "disconnected":
+ displayedName = Not connected ;
+ break;
+ }
+ }
+
+ return (
+ {
+ if(props.active) {
+ return;
+ }
+
+ events.fire("action_set_active_handler", { handlerId: props.handlerId });
+ }}
+ >
+
+ {icon}
+
+
{displayedName}
+
{
+ events.fire("action_destroy_handler", { handlerId: props.handlerId })
+ }}>
+
+
+
+ );
+}
+
+const HandlerList = (props: { refContainer: React.Ref, refSpacer: React.Ref }) => {
+ const events = useContext(Events);
+
+ const [ handlers, setHandlers ] = useState(() => {
+ events.fire("query_handler_list");
+ return "loading";
+ });
+
+ const [ activeHandler, setActiveHandler ] = useState();
+
+ events.reactUse("notify_handler_list", event => {
+ setHandlers(event.handlerIds.slice());
+ setActiveHandler(event.activeHandlerId);
+ });
+ events.reactUse("notify_active_handler", event => setActiveHandler(event.handlerId));
+
+ return (
+
+ {handlers === "loading" ? undefined :
+ handlers.map(handlerId =>
)
+ }
+
+
+ )
+}
+
+const ScrollMenu = (props: { shown: boolean }) => {
+ const events = useContext(Events);
+
+ const [ scrollLeft, setScrollLeft ] = useState(false);
+ const [ scrollRight, setScrollRight ] = useState(false);
+
+ events.on("notify_scroll_status", event => {
+ setScrollLeft(event.left);
+ setScrollRight(event.right);
+ });
+
+ return (
+
+
scrollLeft && events.fire_async("action_scroll", { direction: "left" })}>
+
+
+
scrollRight && events.fire_async("action_scroll", { direction: "right" })}>
+
+
+
+ );
+}
+
+export const ConnectionHandlerList = (props: { events: Registry }) => {
+ const [ shown, setShown ] = useState(false);
+ const observer = useRef();
+ const refHandlerContainer = useRef();
+ const refContainer = useRef();
+ const refScrollSpacer = useRef();
+
+ const refScrollShown = useRef(false);
+ const [ scrollShown, setScrollShown ] = useState(false);
+ refScrollShown.current = scrollShown;
+
+ const updateScrollButtons = (scrollLeft: number) => {
+ const container = refHandlerContainer.current;
+ if(!container) { return; }
+ props.events.fire_async("notify_scroll_status", { right: Math.ceil(scrollLeft + container.clientWidth + 2) < container.scrollWidth, left: scrollLeft !== 0 });
+ }
+
+ useEffect(() => {
+ if(!refHandlerContainer.current) {
+ return;
+ }
+
+ if(observer.current) {
+ throw "useEffect called without a detachment...";
+ }
+
+ observer.current = new ResizeObserver(events => {
+ if(!refContainer.current || !refHandlerContainer.current) {
+ return;
+ }
+
+ if(events.length !== 1) {
+ logWarn(LogCategory.CLIENT, tr("Handler list resize observer received events for more than one element (%d elements)."), events.length);
+ return;
+ }
+
+ const width = events[0].target.scrollWidth;
+ const shouldScroll = width > refContainer.current.clientWidth;
+
+ let scrollShown = refScrollShown.current;
+ if(scrollShown && !shouldScroll) {
+ props.events.fire_async("notify_scroll_status", { left: false, right: false });
+ } else if(!scrollShown && shouldScroll) {
+ props.events.fire_async("notify_scroll_status", { left: false, right: true });
+ } else {
+ return;
+ }
+
+ setScrollShown(shouldScroll);
+ });
+ observer.current.observe(refHandlerContainer.current);
+ return () => {
+ observer.current?.disconnect();
+ observer.current = undefined;
+ }
+ }, []);
+
+ props.events.reactUse("notify_handler_list", event => setShown(event.handlerIds.length > 1));
+ props.events.reactUse("action_scroll", event => {
+ if(!scrollShown || !refHandlerContainer.current || !refScrollSpacer.current) {
+ return;
+ }
+
+ const container = refHandlerContainer.current;
+ const scrollContainer = refScrollSpacer.current;
+ const getChildAt = (width: number) => {
+ let currentChild: HTMLDivElement;
+ for(const childElement of container.children) {
+ const child = childElement as HTMLDivElement;
+ if((!currentChild || child.offsetLeft > currentChild.offsetLeft) && child.offsetLeft <= width) {
+ currentChild = child;
+ }
+ }
+
+ return currentChild;
+ }
+
+ let scrollLeft;
+ if(event.direction === "right") {
+ const currentLeft = container.scrollLeft;
+ const target = getChildAt(currentLeft + container.clientWidth + 5 - scrollContainer.clientWidth);
+ if(!target) {
+ /* well we're fucked up? :D */
+ scrollLeft = container.scrollLeft + 50;
+ } else {
+ scrollLeft = target.offsetLeft + target.clientWidth - container.clientWidth + scrollContainer.clientWidth;
+ }
+ } else if(event.direction === "left") {
+ const currentLeft = container.scrollLeft;
+ const target = getChildAt(currentLeft - 1);
+ if(!target) {
+ /* well we're fucked up? :D */
+ scrollLeft = container.scrollLeft - 50;
+ } else {
+ scrollLeft = target.offsetLeft;
+ }
+ } else {
+ return;
+ }
+
+ container.scrollLeft = scrollLeft;
+ updateScrollButtons(scrollLeft);
+ }, true, [scrollShown]);
+ return (
+
+
+
+
+
+
+ )
+};
\ No newline at end of file
diff --git a/shared/js/ui/frames/control-bar/index.tsx b/shared/js/ui/frames/control-bar/index.tsx
index 3a3fdca1..88d86774 100644
--- a/shared/js/ui/frames/control-bar/index.tsx
+++ b/shared/js/ui/frames/control-bar/index.tsx
@@ -434,12 +434,12 @@ export class ControlBar extends React.Component {
}
private handleActiveConnectionHandlerChanged(event: ConnectionManagerEvents["notify_active_handler_changed"]) {
- if(event.old_handler)
- this.unregisterConnectionHandlerEvents(event.old_handler);
+ if(event.oldHandler)
+ this.unregisterConnectionHandlerEvents(event.oldHandler);
- this.connection = event.new_handler;
- if(event.new_handler)
- this.registerConnectionHandlerEvents(event.new_handler);
+ this.connection = event.newHandler;
+ if(event.newHandler)
+ this.registerConnectionHandlerEvents(event.newHandler);
this.event_registry.fire("set_connection_handler", { handler: this.connection });
this.event_registry.fire("update_state_all");
diff --git a/shared/js/ui/modal/css-editor/Renderer.tsx b/shared/js/ui/modal/css-editor/Renderer.tsx
index 218d4ab1..030007e0 100644
--- a/shared/js/ui/modal/css-editor/Renderer.tsx
+++ b/shared/js/ui/modal/css-editor/Renderer.tsx
@@ -217,16 +217,17 @@ const OverrideVariableInfo = (props: { events: Registry }) => {
selectedVariable.overwriteValue = event.enabled;
setOverwriteEnabled(event.enabled);
- if (event.enabled)
+ if (event.enabled) {
setOverwriteValue(event.value);
- });
+ }
+ }, true, [selectedVariable]);
props.events.reactUse("action_change_override_value", event => {
if (event.variableName !== selectedVariable?.name)
return;
setOverwriteValue(event.value);
- });
+ }, true, [selectedVariable]);
return (<>
diff --git a/shared/js/ui/modal/settings/MicrophoneRenderer.tsx b/shared/js/ui/modal/settings/MicrophoneRenderer.tsx
index b96373f2..a2447bca 100644
--- a/shared/js/ui/modal/settings/MicrophoneRenderer.tsx
+++ b/shared/js/ui/modal/settings/MicrophoneRenderer.tsx
@@ -78,7 +78,7 @@ const ActivityBar = (props: { events: Registry
, device
setStatus({mode: "error", message: device.error + ""});
}
}
- });
+ }, true, [status]);
let error;
diff --git a/shared/js/ui/react-elements/Icon.tsx b/shared/js/ui/react-elements/Icon.tsx
index d1295901..99c53606 100644
--- a/shared/js/ui/react-elements/Icon.tsx
+++ b/shared/js/ui/react-elements/Icon.tsx
@@ -1,26 +1,26 @@
import * as React from "react";
import {LocalIcon} from "tc-shared/file/Icons";
-export interface IconProperties {
+export const IconRenderer = (props: {
icon: string | LocalIcon;
title?: string;
-}
-
-export class IconRenderer extends React.Component {
- render() {
- if(!this.props.icon)
- return
;
- else if(typeof this.props.icon === "string")
- return
;
- else if(this.props.icon instanceof LocalIcon)
- return ;
- else throw "JQuery icons are not longer supported";
+ className?: string;
+}) => {
+ if(!props.icon) {
+ return
;
+ } else if(typeof props.icon === "string") {
+ return
;
+ } else if(props.icon instanceof LocalIcon) {
+ return ;
+ } else {
+ throw "JQuery icons are not longer supported";
}
}
export interface LoadedIconRenderer {
icon: LocalIcon;
title?: string;
+ className?: string;
}
export class LocalIconRenderer extends React.Component {
@@ -39,18 +39,18 @@ export class LocalIconRenderer extends React.Component {
render() {
const icon = this.props.icon;
if(!icon || icon.status === "empty" || icon.status === "destroyed")
- return
;
+ return
;
else if(icon.status === "loaded") {
if(icon.icon_id >= 0 && icon.icon_id <= 1000) {
if(icon.icon_id === 0)
return
;
return
;
}
- return ;
+ return ;
} else if(icon.status === "loading")
- return ;
+ return ;
else if(icon.status === "error")
- return
;
+ return
;
}
componentDidMount(): void {
diff --git a/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx b/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx
index ef1b147c..80c29dd3 100644
--- a/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx
+++ b/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx
@@ -19,11 +19,14 @@ class TitleRenderer {
}
setInstance(instance: AbstractModal) {
- if(this.modalInstance)
+ if(this.modalInstance) {
ReactDOM.unmountComponentAtNode(this.htmlContainer);
+ }
+
this.modalInstance = instance;
- if(this.modalInstance)
+ if(this.modalInstance) {
ReactDOM.render(<>{this.modalInstance.title()}>, this.htmlContainer);
+ }
}
}
diff --git a/shared/js/ui/tree/Client.tsx b/shared/js/ui/tree/Client.tsx
index 6331eee0..97a8f6e9 100644
--- a/shared/js/ui/tree/Client.tsx
+++ b/shared/js/ui/tree/Client.tsx
@@ -20,85 +20,37 @@ import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
import * as DOMPurify from "dompurify";
import {ClientIcon} from "svg-sprites/client-icons";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
+import {useState} from "react";
const clientStyle = require("./Client.scss");
const viewStyle = require("./View.scss");
-interface ClientIconProperties {
- client: ClientEntryController;
-}
+const IconUpdateKeys: (keyof ClientProperties)[] = [
+ "client_away",
+ "client_input_hardware",
+ "client_output_hardware",
+ "client_output_muted",
+ "client_input_muted",
+ "client_is_channel_commander",
+ "client_talk_power"
+];
-@ReactEventHandler(e => e.props.client.events)
-@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
-class ClientSpeakIcon extends ReactComponentBase {
- private static readonly IconUpdateKeys: (keyof ClientProperties)[] = [
- "client_away",
- "client_input_hardware",
- "client_output_hardware",
- "client_output_muted",
- "client_input_muted",
- "client_is_channel_commander",
- "client_talk_power"
- ];
+export const ClientStatusIndicator = (props: { client: ClientEntryController, renderer?: (icon: ClientIcon) => React.ReactElement }) => {
+ const [ revision, setRevision ] = useState(0);
- render() {
- const client = this.props.client;
- const properties = client.properties;
-
- let icon: ClientIcon;
- if (properties.client_type_exact == ClientType.CLIENT_QUERY) {
- icon = ClientIcon.ServerQuery;
- } else {
- if (properties.client_away) {
- icon = ClientIcon.Away;
- } else if (!client.getVoiceClient() && !(this instanceof LocalClientEntry)) {
- icon = ClientIcon.InputMutedLocal;
- } else if (!properties.client_output_hardware) {
- icon = ClientIcon.HardwareOutputMuted;
- } else if (properties.client_output_muted) {
- icon = ClientIcon.OutputMuted;
- } else if (!properties.client_input_hardware) {
- icon = ClientIcon.HardwareInputMuted;
- } else if (properties.client_input_muted) {
- icon = ClientIcon.InputMuted;
- } else {
- if (client.isSpeaking()) {
- if (properties.client_is_channel_commander) {
- icon = ClientIcon.PlayerCommanderOn;
- } else {
- icon = ClientIcon.PlayerOn;
- }
- } else {
- if (properties.client_is_channel_commander) {
- icon = ClientIcon.PlayerCommanderOff;
- } else {
- icon = ClientIcon.PlayerOff;
- }
- }
- }
- }
-
- return
- }
-
- @EventHandler("notify_properties_updated")
- private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) {
- for (const key of ClientSpeakIcon.IconUpdateKeys)
+ props.client.events.reactUse("notify_properties_updated", event => {
+ for (const key of IconUpdateKeys) {
if (key in event.updated_properties) {
- this.forceUpdate();
+ setRevision(revision + 1);
return;
}
- }
+ }
+ });
- @EventHandler("notify_mute_state_change")
- private handleMuteStateChange() {
- this.forceUpdate();
- }
+ props.client.events.reactUse("notify_mute_state_change", () => setRevision(revision + 1));
+ props.client.events.reactUse("notify_speak_state_change", () => setRevision(revision + 1));
- @EventHandler("notify_speak_state_change")
- private handleSpeakStateChange() {
- this.forceUpdate();
- }
+ return props.renderer ? props.renderer(props.client.getStatusIcon()) : ;
}
interface ClientServerGroupIconsProperties {
@@ -390,7 +342,7 @@ export class ClientEntry extends TreeEntry this.onContextMenu(e)}
>
-
+
{this.state.rename ?
this.onEditFinished(name)}
initialName={this.state.renameInitialName || this.props.client.properties.client_nickname}/> :
diff --git a/web/app/index.ts b/web/app/index.ts
index dbdcaee6..a2f50861 100644
--- a/web/app/index.ts
+++ b/web/app/index.ts
@@ -12,4 +12,7 @@ import "./hooks/AudioRecorder";
import "./UnloadHandler";
+
+import "./ui/FaviconRenderer";
+
export = require("tc-shared/main");
\ No newline at end of file
diff --git a/web/app/ui/FaviconRenderer.tsx b/web/app/ui/FaviconRenderer.tsx
new file mode 100644
index 00000000..46fb0e69
--- /dev/null
+++ b/web/app/ui/FaviconRenderer.tsx
@@ -0,0 +1,86 @@
+import * as loader from "tc-loader";
+import {Stage} from "tc-loader";
+import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
+import * as React from "react";
+import {useState} from "react";
+import * as ReactDOM from "react-dom";
+import {ClientStatusIndicator} from "tc-shared/ui/tree/Client";
+import {server_connections} from "tc-shared/ConnectionManager";
+
+import {
+ ClientIcon,
+ spriteEntries as kClientSpriteEntries,
+ spriteUrl as kClientSpriteUrl
+} from "svg-sprites/client-icons";
+
+let iconImage: HTMLImageElement;
+async function initializeFaviconRenderer() {
+ iconImage = new Image();
+ iconImage.src = kClientSpriteUrl;
+ await new Promise((resolve, reject) => {
+ iconImage.onload = resolve;
+ iconImage.onerror = () => reject("failed to load client icon sprite");
+ });
+
+ let container = document.createElement("span");
+ ReactDOM.render(ReactDOM.createPortal( , document.head), container, () => {
+ document.getElementById("favicon").remove();
+ });
+ //container.remove();
+}
+
+function clientIconToDataUrl(icon: ClientIcon) : string | undefined {
+ const sprite = kClientSpriteEntries.find(e => e.className === icon);
+ if(!sprite) {
+ return;
+ }
+
+ const canvas = document.createElement("canvas");
+ canvas.width = sprite.width;
+ canvas.height = sprite.height;
+
+ const context = canvas.getContext("2d");
+ context.drawImage(iconImage, sprite.xOffset, sprite.yOffset, sprite.width, sprite.height, 0, 0, sprite.width, sprite.height);
+
+ return canvas.toDataURL();
+}
+
+const FaviconRenderer = () => {
+ const [ handler, setHandler ] = useState(server_connections.active_connection());
+
+ server_connections.events().reactUse("notify_active_handler_changed", event => setHandler(event.newHandler));
+
+ return handler ? : ;
+};
+
+const DefaultFaviconRenderer = () => ;
+const ClientIconFaviconRenderer = (props: { icon: ClientIcon }) => {
+ const url = clientIconToDataUrl(props.icon);
+ if(!url) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+const HandlerFaviconRenderer = (props: { connection: ConnectionHandler }) => {
+ const [ showClientStatus, setShowClientStatus ] = useState(props.connection.connection_state === ConnectionState.CONNECTED);
+ props.connection.events().reactUse("notify_connection_state_changed", event => setShowClientStatus(event.new_state === ConnectionState.CONNECTED));
+
+ if(showClientStatus) {
+ return }
+ key={"server"}
+ />;
+ } else {
+ return ;
+ }
+}
+
+
+loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
+ name: "favicon renderer",
+ function: initializeFaviconRenderer,
+ priority: 10
+});
\ No newline at end of file
diff --git a/web/app/voice/VoiceHandler.ts b/web/app/voice/VoiceHandler.ts
index e5d8bf6b..5b8f8c2e 100644
--- a/web/app/voice/VoiceHandler.ts
+++ b/web/app/voice/VoiceHandler.ts
@@ -25,6 +25,7 @@ import {
} from "tc-shared/voice/VoiceWhisper";
import {VoiceClient} from "tc-shared/voice/VoiceClient";
import {WebWhisperSession} from "tc-backend/web/voice/VoiceWhisper";
+import {VoicePlayerState} from "tc-shared/voice/VoicePlayer";
export enum VoiceEncodeType {
JS_ENCODE,
@@ -68,6 +69,10 @@ export class VoiceConnection extends AbstractVoiceConnection {
private lastConnectAttempt: number = 0;
+ private currentlyReplayingVoice: boolean = false;
+ private readonly voiceClientStateChangedEventListener;
+ private readonly whisperSessionStateChangedEventListener;
+
constructor(connection: ServerConnection) {
super(connection);
@@ -80,6 +85,9 @@ export class VoiceConnection extends AbstractVoiceConnection {
this.connection.events.on("notify_connection_state_changed",
this.serverConnectionStateListener = this.handleServerConnectionStateChanged.bind(this));
+
+ this.voiceClientStateChangedEventListener = this.handleVoiceClientStateChange.bind(this);
+ this.whisperSessionStateChangedEventListener = this.handleWhisperSessionStateChange.bind(this);
}
getConnectionState(): VoiceConnectionStatus {
@@ -341,6 +349,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
if(!(client instanceof VoiceClientController))
throw "Invalid client type";
+ client.events.off("notify_state_changed", this.voiceClientStateChangedEventListener);
delete this.voiceClients[client.getClientId()];
client.destroy();
}
@@ -352,6 +361,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
const client = new VoiceClientController(clientId);
this.voiceClients[clientId] = client;
+ client.events.on("notify_state_changed", this.voiceClientStateChangedEventListener);
return client;
}
@@ -371,6 +381,41 @@ export class VoiceConnection extends AbstractVoiceConnection {
this.encoderCodec = codec;
}
+ stopAllVoiceReplays() {
+ this.availableVoiceClients().forEach(e => e.abortReplay());
+ /* TODO: Whisper sessions as well */
+ }
+
+ isReplayingVoice(): boolean {
+ return this.currentlyReplayingVoice;
+ }
+
+ private handleVoiceClientStateChange(/* event: VoicePlayerEvents["notify_state_changed"] */) {
+ this.updateVoiceReplaying();
+ }
+
+ private handleWhisperSessionStateChange() {
+ this.updateVoiceReplaying();
+ }
+
+ private updateVoiceReplaying() {
+ let replaying = false;
+ if(this.connectionState === VoiceConnectionStatus.Connected) {
+ let index = this.availableVoiceClients().findIndex(client => client.getState() === VoicePlayerState.PLAYING || client.getState() === VoicePlayerState.BUFFERING);
+ replaying = index !== -1;
+
+ if(!replaying) {
+ index = this.getWhisperSessions().findIndex(session => session.getSessionState() === WhisperSessionState.PLAYING);
+ replaying = index !== -1;
+ }
+ }
+
+ if(this.currentlyReplayingVoice !== replaying) {
+ this.currentlyReplayingVoice = replaying;
+ this.events.fire_async("notify_voice_replay_state_change", { replaying: replaying });
+ }
+ }
+
protected handleWhisperPacket(packet: VoiceWhisperPacket) {
const clientId = packet.clientId;
@@ -378,6 +423,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
if(typeof session !== "object") {
logDebug(LogCategory.VOICE, tr("Received new whisper from %d (%s)"), packet.clientId, packet.clientNickname);
session = (this.whisperSessions[clientId] = new WebWhisperSession(packet));
+ session.events.on("notify_state_changed", this.whisperSessionStateChangedEventListener);
this.whisperSessionInitializer(session).then(result => {
session.initializeFromData(result).then(() => {
if(this.whisperSessions[clientId] !== session) {
@@ -413,6 +459,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
throw tr("Session isn't an instance of the web whisper system");
}
+ session.events.off("notify_state_changed", this.whisperSessionStateChangedEventListener);
delete this.whisperSessions[session.getClientId()];
session.destroy();
}
diff --git a/web/app/voice/VoicePlayer.ts b/web/app/voice/VoicePlayer.ts
index e3b9bbbe..ab184af6 100644
--- a/web/app/voice/VoicePlayer.ts
+++ b/web/app/voice/VoicePlayer.ts
@@ -69,7 +69,7 @@ export class WebVoicePlayer implements VoicePlayer {
}
getState(): VoicePlayerState {
- return undefined;
+ return this.playerState;
}
getVolume(): number {
@@ -118,6 +118,7 @@ export class WebVoicePlayer implements VoicePlayer {
destroy() {
this.audioClient?.destroy();
this.audioClient = undefined;
+ this.events.destroy();
}
private initializeAudio() : Promise {
@@ -252,6 +253,7 @@ export class WebVoicePlayer implements VoicePlayer {
} else if(this.playerState === VoicePlayerState.PLAYING) {
logDebug(LogCategory.VOICE, tr("Voice player has a buffer underflow. Changing state to buffering."));
this.setPlayerState(VoicePlayerState.BUFFERING);
+ this.resetBufferTimeout(true);
}
}
diff --git a/webpack.config.ts b/webpack.config.ts
index 93323930..52a2bba1 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -67,7 +67,7 @@ export const config = async (target: "web" | "client"): Promise =
devtool: isDevelopment ? "inline-source-map" : undefined,
mode: isDevelopment ? "development" : "production",
plugins: [
- new CleanWebpackPlugin(),
+ //new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'