Adding popout to channel conversations and fixed doubling of chat messages

master
WolverinDEV 2021-04-29 14:51:30 +02:00
parent 32de3542df
commit 1cae741b17
24 changed files with 362 additions and 86 deletions

View File

@ -1,4 +1,8 @@
# Changelog:
* **29.04.21**
- Fixed a bug which caused chat messages to appear twice
- Adding support for poping out channel conversations
* **27.04.21**
- Implemented support for showing the video feed watchers
- Updating the channel tree if the channel client order changes

View File

@ -11,9 +11,9 @@ export default class implements ApplicationLoader {
console.log("Doing nothing");
for(let index of [1, 2, 3]) {
await new Promise(resolve => {
await new Promise<void>(resolve => {
const callback = () => {
document.removeEventListener("click", resolve);
document.removeEventListener("click", callback);
resolve();
};

View File

@ -50,6 +50,7 @@ const ContentRendererServer = () => {
handlerId={contentData.handlerId}
messagesDeletable={true}
noFirstMessageOverlay={false}
popoutable={true}
/>
);
};

View File

@ -1,5 +1,5 @@
import {AbstractConversationUiEvents, ChatHistoryState} from "./AbstractConversationDefinitions";
import {EventHandler, Registry} from "../../../events";
import {EventHandler, Registry} from "tc-events";
import {LogCategory, logError} from "../../../log";
import {tr, tra} from "../../../i18n/localize";
import {
@ -8,9 +8,12 @@ import {
AbstractChatManagerEvents,
AbstractConversationEvents
} from "tc-shared/conversations/AbstractConversion";
import {ChannelConversation} from "tc-shared/conversations/ChannelConversationManager";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */
export type SelectedConversation<ConversationType> = ConversationType | undefined | "conversation-manager-selected";
export abstract class AbstractConversationController<
Events extends AbstractConversationUiEvents,
Manager extends AbstractChatManager<ManagerEvents, ConversationType, ConversationEvents>,
@ -20,15 +23,17 @@ export abstract class AbstractConversationController<
> {
protected readonly uiEvents: Registry<Events>;
protected conversationManager: Manager | undefined;
protected listenerManager: (() => void)[];
private listenerManager: (() => void)[];
protected currentSelectedConversation: ConversationType;
protected currentSelectedListener: (() => void)[];
private selectedConversation: SelectedConversation<ConversationType>;
private currentSelectedConversation: ConversationType;
private currentSelectedListener: (() => void)[];
protected constructor() {
this.uiEvents = new Registry<Events>();
this.currentSelectedListener = [];
this.listenerManager = [];
this.selectedConversation = "conversation-manager-selected";
}
destroy() {
@ -50,27 +55,48 @@ export abstract class AbstractConversationController<
this.listenerManager.forEach(callback => callback());
this.listenerManager = [];
this.conversationManager = manager;
if(manager) {
this.registerConversationManagerEvents(manager);
this.setCurrentlySelected(manager.getSelectedConversation());
} else {
this.setCurrentlySelected(undefined);
this.conversationManager = manager;
this.selectedConversation = undefined;
this.setCurrentlySelected(undefined);
if(this.conversationManager) {
this.registerConversationManagerEvents(this.conversationManager);
}
}
protected registerConversationManagerEvents(manager: Manager) {
this.listenerManager.push(manager.events.on("notify_selected_changed", event => this.setCurrentlySelected(event.newConversation)));
protected setSelectedConversation(conversation: SelectedConversation<ConversationType>) {
if(this.selectedConversation === conversation) {
return;
}
/* TODO: Verify that that conversation matches our current handler? */
this.selectedConversation = conversation;
if(conversation === "conversation-manager-selected") {
this.setCurrentlySelected(this.conversationManager?.getSelectedConversation());
} else {
this.setCurrentlySelected(conversation);
}
}
protected registerConversationManagerEvents(manager: Manager) : (() => void)[] {
this.listenerManager.push(manager.events.on("notify_selected_changed", event => {
if(this.selectedConversation === "conversation-manager-selected") {
this.setCurrentlySelected(event.newConversation);
}
}));
this.listenerManager.push(manager.events.on("notify_cross_conversation_support_changed", () => {
const currentConversation = this.getCurrentConversation();
if(currentConversation) {
this.reportStateToUI(currentConversation);
}
}));
return this.listenerManager;
}
protected registerConversationEvents(conversation: ConversationType) {
protected registerConversationEvents(conversation: ConversationType) : (() => void)[] {
this.currentSelectedListener.push(conversation.events.on("notify_unread_timestamp_changed", event =>
this.uiEvents.fire_react("notify_unread_timestamp_changed", { chatId: conversation.getChatId(), timestamp: event.timestamp })));
@ -92,9 +118,11 @@ export abstract class AbstractConversationController<
this.currentSelectedListener.push(conversation.events.on("notify_read_state_changed", () => {
this.reportStateToUI(conversation);
}));
return this.currentSelectedListener;
}
protected setCurrentlySelected(conversation: ConversationType | undefined) {
private setCurrentlySelected(conversation: ConversationType | undefined) {
if(this.currentSelectedConversation === conversation) {
return;
}
@ -182,6 +210,7 @@ export abstract class AbstractConversationController<
}
}
public uiQueryHistory(conversation: AbstractChat<any>, timestamp: number, enforce?: boolean) {
const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()] || (this.conversationManager.historyUiStates[conversation.getChatId()] = {
executingUIHistoryQuery: false,

View File

@ -119,6 +119,7 @@ export interface AbstractConversationUiEvents {
action_send_message: { text: string, chatId: string },
action_jump_to_present: { chatId: string },
action_focus_chat: {},
action_popout_chat: {},
query_selected_chat: {},
/* will cause a notify_selected_chat */

View File

@ -52,6 +52,11 @@ html:root {
width: 100%;
min-width: 250px;
min-height: 10em;
flex-grow: 1;
flex-shrink: 1;
background: var(--chat-background);
position: relative;

View File

@ -24,6 +24,8 @@ import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
import {ColloquialFormat, date_format, format_date_general, formatDayTime} from "tc-shared/utils/DateUtils";
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
import {ChatBox} from "tc-shared/ui/react-elements/ChatBox";
import {DetachButton} from "tc-shared/ui/react-elements/DetachButton";
import {useTr} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./AbstractConversationRenderer.scss");
@ -319,12 +321,14 @@ const PartnerTypingIndicator = (props: { events: Registry<AbstractConversationUi
});
props.events.reactUse("notify_chat_event", event => {
if(event.chatId !== props.chatId)
if(event.chatId !== props.chatId) {
return;
}
if(event.event.type === "message") {
if(!event.event.isOwnMessage)
if(!event.event.isOwnMessage) {
setTypingTimestamp(0);
}
} else if(event.event.type === "partner-action" || event.event.type === "local-action") {
setTypingTimestamp(0);
}
@ -774,11 +778,13 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
@EventHandler<AbstractConversationUiEvents>("notify_chat_event")
private handleChatEvent(event: AbstractConversationUiEvents["notify_chat_event"]) {
if(event.chatId !== this.currentChatId || this.state.isBrowsingHistory)
if(event.chatId !== this.currentChatId || this.state.isBrowsingHistory) {
return;
}
if(event.event.type === "local-user-switch" && !this.showSwitchEvents)
if(event.event.type === "local-user-switch" && !this.showSwitchEvents) {
return;
}
this.chatEvents.push(event.event);
this.sortEvents();
@ -877,7 +883,13 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
}
}
export const ConversationPanel = React.memo((props: { events: Registry<AbstractConversationUiEvents>, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => {
export const ConversationPanel = React.memo((props: {
events: Registry<AbstractConversationUiEvents>,
handlerId: string,
messagesDeletable: boolean,
noFirstMessageOverlay: boolean,
popoutable: boolean
}) => {
const currentChat = useRef({ id: "unselected" });
const chatEnabled = useRef(false);
@ -910,11 +922,19 @@ export const ConversationPanel = React.memo((props: { events: Registry<AbstractC
return refChatBox.current.events.on("notify_typing", () => props.events.fire("action_self_typing", { chatId: currentChat.current.id }));
});
return <div className={cssStyle.panel}>
<ConversationMessages events={props.events} handlerId={props.handlerId} messagesDeletable={props.messagesDeletable} noFirstMessageOverlay={props.noFirstMessageOverlay} />
<ChatBox
ref={refChatBox}
onSubmit={text => props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) }
/>
</div>
return (
<DetachButton
disabled={!props.popoutable}
detached={false}
callbackToggle={() => props.events.fire("action_popout_chat")}
detachText={useTr("Open in a new window")}
className={cssStyle.panel}
>
<ConversationMessages events={props.events} handlerId={props.handlerId} messagesDeletable={props.messagesDeletable} noFirstMessageOverlay={props.noFirstMessageOverlay} />
<ChatBox
ref={refChatBox}
onSubmit={text => props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) }
/>
</DetachButton>
)
});

View File

@ -58,6 +58,7 @@ const ModeRendererConversation = React.memo(() => {
handlerId={channelContext.handlerId}
messagesDeletable={true}
noFirstMessageOverlay={false}
popoutable={true}
/>
);
});

View File

@ -3,7 +3,7 @@ import {EventHandler} from "tc-events";
import {LogCategory, logError} from "../../../log";
import {tr} from "../../../i18n/localize";
import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions";
import {AbstractConversationController} from "./AbstractConversationController";
import {AbstractConversationController, SelectedConversation} from "./AbstractConversationController";
import {
ChannelConversation,
ChannelConversationEvents,
@ -11,6 +11,7 @@ import {
ChannelConversationManagerEvents
} from "tc-shared/conversations/ChannelConversationManager";
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
import {spawnModalChannelChat} from "tc-shared/ui/modal/channel-chat/Controller";
export class ChannelConversationController extends AbstractConversationController<
ChannelConversationUiEvents,
@ -26,17 +27,15 @@ export class ChannelConversationController extends AbstractConversationControlle
super();
this.connectionListener = [];
/*
spawnExternalModal("conversation", this.uiEvents, {
handlerId: this.connection.handlerId,
noFirstMessageOverlay: false,
messagesDeletable: true
}).open().then(() => {
console.error("Opened");
});
*/
this.uiEvents.registerHandler(this, true);
this.uiEvents.on("action_popout_chat", () => {
const conversation = this.getCurrentConversation();
if(!conversation) {
return;
}
spawnModalChannelChat(this.connection, conversation);
});
}
destroy() {
@ -59,11 +58,16 @@ export class ChannelConversationController extends AbstractConversationControlle
if(connection) {
/* FIXME: Update cross channel talk state! */
this.setConversationManager(connection.getChannelConversations());
this.setSelectedConversation("conversation-manager-selected");
} else {
this.setConversationManager(undefined);
}
}
setSelectedConversation(conversation: SelectedConversation<ChannelConversation>) {
super.setSelectedConversation(conversation);
}
@EventHandler<AbstractConversationUiEvents>("action_delete_message")
private handleMessageDelete(event: AbstractConversationUiEvents["action_delete_message"]) {
const conversation = this.conversationManager?.findConversationById(event.chatId);
@ -75,15 +79,17 @@ export class ChannelConversationController extends AbstractConversationControlle
conversation.deleteMessage(event.uniqueId);
}
protected registerConversationEvents(conversation: ChannelConversation) {
super.registerConversationEvents(conversation);
protected registerConversationEvents(conversation: ChannelConversation): (() => void)[] {
const events = super.registerConversationEvents(conversation);
this.currentSelectedListener.push(conversation.events.on("notify_messages_deleted", event => {
events.push(conversation.events.on("notify_messages_deleted", event => {
this.uiEvents.fire_react("notify_chat_message_delete", { messageIds: event.messages, chatId: conversation.getChatId() });
}));
this.currentSelectedListener.push(conversation.events.on("notify_conversation_mode_changed", () => {
events.push(conversation.events.on("notify_conversation_mode_changed", () => {
this.reportStateToUI(conversation);
}));
return events;
}
}

View File

@ -1,3 +1,3 @@
import {AbstractConversationUiEvents} from "tc-shared/ui/frames/side/AbstractConversationDefinitions";
export interface ChannelConversationUiEvents extends AbstractConversationUiEvents {}
export interface ChannelConversationUiEvents extends AbstractConversationUiEvents { }

View File

@ -20,7 +20,9 @@ class PopoutConversationRenderer extends AbstractModal {
handlerId={this.userData.handlerId}
events={this.events}
messagesDeletable={this.userData.messagesDeletable}
noFirstMessageOverlay={this.userData.noFirstMessageOverlay} />;
noFirstMessageOverlay={this.userData.noFirstMessageOverlay}
popoutable={false}
/>;
}
renderTitle() {

View File

@ -1,5 +1,5 @@
import {ConnectionHandler} from "../../../ConnectionHandler";
import {EventHandler} from "../../../events";
import {EventHandler} from "tc-events";
import {
PrivateConversationInfo,
PrivateConversationUIEvents
@ -74,16 +74,17 @@ export class PrivateConversationController extends AbstractConversationControlle
this.connection = connection;
if(connection) {
this.setConversationManager(connection.getPrivateConversations());
this.setSelectedConversation("conversation-manager-selected");
} else {
this.setConversationManager(undefined);
}
this.reportConversationList();
}
protected registerConversationManagerEvents(manager: PrivateConversationManager) {
super.registerConversationManagerEvents(manager);
protected registerConversationManagerEvents(manager: PrivateConversationManager): (() => void)[] {
const events = super.registerConversationManagerEvents(manager);
this.listenerManager.push(manager.events.on("notify_conversation_created", event => {
events.push(manager.events.on("notify_conversation_created", event => {
const conversation = event.conversation;
const events = this.listenerConversation[conversation.getChatId()] = [];
events.push(conversation.events.on("notify_partner_changed", event => {
@ -101,17 +102,18 @@ export class PrivateConversationController extends AbstractConversationControlle
this.reportConversationList();
}));
this.listenerManager.push(manager.events.on("notify_conversation_destroyed", event => {
events.push(manager.events.on("notify_conversation_destroyed", event => {
this.listenerConversation[event.conversation.getChatId()]?.forEach(callback => callback());
delete this.listenerConversation[event.conversation.getChatId()];
this.reportConversationList();
}));
this.listenerManager.push(manager.events.on("notify_selected_changed", () => this.reportConversationList()));
this.listenerManager.push(() => {
events.push(manager.events.on("notify_selected_changed", () => this.reportConversationList()));
events.push(() => {
Object.values(this.listenerConversation).forEach(callbacks => callbacks.forEach(callback => callback()));
this.listenerConversation = {};
});
return events;
}
focusInput() {

View File

@ -220,7 +220,7 @@ export const PrivateConversationsPanel = (props: { events: Registry<PrivateConve
<div className={cssStyle.dividerContainer}>
<OpenConversationsPanel />
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider} />
<ConversationPanel events={props.events as any} handlerId={props.handlerId} noFirstMessageOverlay={true} messagesDeletable={false} />
<ConversationPanel events={props.events as any} handlerId={props.handlerId} noFirstMessageOverlay={true} messagesDeletable={false} popoutable={false} />
</div>
</EventContext.Provider>
</HandlerIdContext.Provider>

View File

@ -0,0 +1,27 @@
import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController";
import {spawnModal} from "tc-shared/ui/react-elements/modal";
import {ignorePromise} from "tc-shared/proto";
import {ChannelConversation} from "tc-shared/conversations/ChannelConversationManager";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
export function spawnModalChannelChat(connectionHandler: ConnectionHandler, conversation: ChannelConversation) {
const channel = connectionHandler.channelTree.findChannel(conversation.conversationId);
const controller = new ChannelConversationController();
controller.setConnectionHandler(connectionHandler);
controller.setSelectedConversation(conversation);
const modal = spawnModal("channel-chat", [{
handlerId: connectionHandler.handlerId,
channelId: typeof channel === "undefined" ? 0 : channel.channelId,
channelName: typeof channel === "undefined" ? "Unknown channel" : channel.channelName(),
events: controller.getUiEvents().generateIpcDescription()
}], {
popoutable: false,
popedOut: true,
uniqueId: "chan-conv-" + connectionHandler.handlerId + "-" + conversation.getChatId()
});
modal.getEvents().on("destroy", () => controller.destroy());
ignorePromise(modal.show());
}

View File

@ -0,0 +1,9 @@
import {IpcRegistryDescription} from "tc-events";
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
export interface ModalChannelChatParameters {
events: IpcRegistryDescription<ChannelConversationUiEvents>,
channelName: string,
channelId: number,
handlerId: string
}

View File

@ -0,0 +1,20 @@
.container {
display: flex;
flex-direction: column;
justify-content: stretch;
min-width: 20em;
min-height: 20em;
max-width: 100%;
max-height: calc(100vh - 10em);
width: 60em;
&.windowed {
width: 100%;
height: 100%;
max-height: 100%;
}
}

View File

@ -0,0 +1,58 @@
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import React from "react";
import {ModalChannelChatParameters} from "tc-shared/ui/modal/channel-chat/Definitions";
import {Registry} from "tc-events";
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
import {ChannelTag} from "tc-shared/ui/tree/EntryTags";
import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./Renderer.scss");
class Modal extends AbstractModal {
private readonly parameters: ModalChannelChatParameters;
private readonly events: Registry<ChannelConversationUiEvents>;
constructor(parameters: ModalChannelChatParameters) {
super();
this.parameters = parameters;
this.events = Registry.fromIpcDescription(parameters.events);
}
protected onDestroy() {
super.onDestroy();
this.events.destroy();
}
renderBody(): React.ReactElement {
return (
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
<ConversationPanel
events={this.events}
handlerId={this.parameters.handlerId}
messagesDeletable={true}
noFirstMessageOverlay={false}
popoutable={false}
/>
</div>
);
}
renderTitle(): string | React.ReactElement {
return (
<VariadicTranslatable text={"Channel Conversation: {}"}>
<ChannelTag
channelName={this.parameters.channelName}
channelId={this.parameters.channelId}
handlerId={this.parameters.handlerId}
style={"text-only"}
/>
</VariadicTranslatable>
);
}
}
export default Modal;

View File

@ -0,0 +1,28 @@
import * as React from "react";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
import {ClientIcon} from "svg-sprites/client-icons";
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./DetachButtons.scss");
export const DetachButton = React.memo((props: {
detached: boolean,
callbackToggle: () => void,
detachText?: string,
attachText?: string,
disabled?: boolean,
className?: string,
children,
}) => {
return (
<div className={joinClassList(cssStyle.container, props.className)}>
<div className={joinClassList(cssStyle.containerButton, props.disabled && cssStyle.disabled)} onClick={props.callbackToggle} key={"overlay"}>
<div className={cssStyle.button} title={props.detached ? props.attachText || tr("Attach element") : props.detachText || tr("Detach element")}>
<ClientIconRenderer icon={props.detached ? ClientIcon.ChannelPopin : ClientIcon.ChannelPopout} />
</div>
</div>
{props.children}
</div>
);
});

View File

@ -0,0 +1,46 @@
@import "../../../css/static/mixin";
@import "../../../css/static/properties";
.container {
position: relative;
overflow: hidden;
&:hover {
.containerButton {
top: 1em;
}
}
}
.containerButton {
position: absolute;
z-index: 10;
top: -3em;
right: 1em;
@include transition(all ease-in-out $button_hover_animation_time);
&.disabled {
display: none;
}
}
.button {
display: flex;
flex-direction: column;
justify-content: center;
border-radius: 50%;
background-color: #0000004f;
padding: .6em;
cursor: pointer;
@include transition(all ease-in-out $button_hover_animation_time);
&:hover {
background-color: #0000008f;
}
}

View File

@ -27,6 +27,7 @@ import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/De
import {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
import {ModalChannelInfoEvents, ModalChannelInfoVariables} from "tc-shared/ui/modal/channel-info/Definitions";
import {ModalVideoViewersEvents, ModalVideoViewersVariables} from "tc-shared/ui/modal/video-viewers/Definitions";
import {ModalChannelChatParameters} from "tc-shared/ui/modal/channel-chat/Definitions";
export type ModalType = "error" | "warning" | "info" | "none";
export type ModalRenderType = "page" | "dialog";
@ -185,6 +186,9 @@ export interface ModalConstructorArguments {
/* events */ IpcRegistryDescription<ModalChannelInfoEvents>,
/* variables */ IpcVariableDescriptor<ModalChannelInfoVariables>
],
"channel-chat": [
/* parameters */ ModalChannelChatParameters
],
"echo-test": [
/* events */ IpcRegistryDescription<EchoTestEvents>
],

View File

@ -43,6 +43,12 @@ registerModal({
popoutSupported: true
});
registerModal({
modalId: "channel-chat",
classLoader: async () => await import("tc-shared/ui/modal/channel-chat/Renderer"),
popoutSupported: true
});
registerModal({
modalId: "echo-test",
classLoader: async () => await import("tc-shared/ui/modal/echo-test/Renderer"),

View File

@ -106,38 +106,45 @@ export const ChannelTag = React.memo((props: {
channelName: string,
channelId: number,
handlerId: string,
className?: string
}) => (
<div
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
onContextMenu={event => {
event.preventDefault();
className?: string,
ipcChannel.sendMessage("contextmenu-channel", {
handlerId: props.handlerId,
channelId: props.channelId,
style?: EntryTagStyle
}) => {
if(props.style === "text-only") {
return <React.Fragment>{props.channelName}</React.Fragment>;
}
return (
<div
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
onContextMenu={event => {
event.preventDefault();
pageX: event.pageX,
pageY: event.pageY
});
}}
draggable={true}
onDragStart={event => {
event.dataTransfer.effectAllowed = "all";
event.dataTransfer.dropEffect = "move";
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.ChannelGreen, name: props.channelName }]), 0, 6);
setupDragData(event.dataTransfer, props.handlerId, [
{
type: "channel",
channelId: props.channelId
}
], "channel");
event.dataTransfer.setData("text/plain", props.channelName);
}}
>
{props.channelName}
</div>
));
ipcChannel.sendMessage("contextmenu-channel", {
handlerId: props.handlerId,
channelId: props.channelId,
pageX: event.pageX,
pageY: event.pageY
});
}}
draggable={true}
onDragStart={event => {
event.dataTransfer.effectAllowed = "all";
event.dataTransfer.dropEffect = "move";
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.ChannelGreen, name: props.channelName }]), 0, 6);
setupDragData(event.dataTransfer, props.handlerId, [
{
type: "channel",
channelId: props.channelId
}
], "channel");
event.dataTransfer.setData("text/plain", props.channelName);
}}
>
{props.channelName}
</div>
);
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "entry tags",

View File

@ -535,7 +535,7 @@ class JavascriptLevelMeter implements LevelMeter {
async initialize() {
try {
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(reject, 5000);
getAudioBackend().executeWhenInitialized(() => {
clearTimeout(timeout);

View File

@ -160,13 +160,13 @@ export class ServerConnection extends AbstractServerConnection {
}));
let timeoutRaised = false;
let timeoutPromise = new Promise(resolve => setTimeout(() => {
let timeoutPromise = new Promise<void>(resolve => setTimeout(() => {
timeoutRaised = true;
resolve();
}, timeout));
let cancelRaised = false;
let cancelPromise = new Promise(resolve => {
let cancelPromise = new Promise<void>(resolve => {
this.connectCancelCallback = () => {
this.connectCancelCallback = undefined;
cancelRaised = true;