From 1cae741b17b6e4e235086abb075cd5daeafa5ca5 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Thu, 29 Apr 2021 14:51:30 +0200 Subject: [PATCH] Adding popout to channel conversations and fixed doubling of chat messages --- ChangeLog.md | 4 ++ loader/app/targets/empty.ts | 4 +- shared/js/ui/frames/SideBarRenderer.tsx | 1 + .../side/AbstractConversationController.ts | 57 ++++++++++++---- .../side/AbstractConversationDefinitions.ts | 1 + .../side/AbstractConversationRenderer.scss | 5 ++ .../side/AbstractConversationRenderer.tsx | 44 ++++++++---- .../js/ui/frames/side/ChannelBarRenderer.tsx | 1 + .../side/ChannelConversationController.ts | 36 +++++----- .../side/ChannelConversationDefinitions.ts | 2 +- .../side/PopoutConversationRenderer.tsx | 4 +- .../side/PrivateConversationController.ts | 16 +++-- .../side/PrivateConversationRenderer.tsx | 2 +- shared/js/ui/modal/channel-chat/Controller.ts | 27 ++++++++ .../js/ui/modal/channel-chat/Definitions.ts | 9 +++ shared/js/ui/modal/channel-chat/Renderer.scss | 20 ++++++ shared/js/ui/modal/channel-chat/Renderer.tsx | 58 ++++++++++++++++ shared/js/ui/react-elements/DetachButton.tsx | 28 ++++++++ .../js/ui/react-elements/DetachButtons.scss | 46 +++++++++++++ .../js/ui/react-elements/modal/Definitions.ts | 4 ++ shared/js/ui/react-elements/modal/Registry.ts | 6 ++ shared/js/ui/tree/EntryTags.tsx | 67 ++++++++++--------- web/app/audio/Recorder.ts | 2 +- web/app/connection/ServerConnection.ts | 4 +- 24 files changed, 362 insertions(+), 86 deletions(-) create mode 100644 shared/js/ui/modal/channel-chat/Controller.ts create mode 100644 shared/js/ui/modal/channel-chat/Definitions.ts create mode 100644 shared/js/ui/modal/channel-chat/Renderer.scss create mode 100644 shared/js/ui/modal/channel-chat/Renderer.tsx create mode 100644 shared/js/ui/react-elements/DetachButton.tsx create mode 100644 shared/js/ui/react-elements/DetachButtons.scss diff --git a/ChangeLog.md b/ChangeLog.md index 6bda93dc..ede28bd1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 diff --git a/loader/app/targets/empty.ts b/loader/app/targets/empty.ts index a4af40b8..417f5af5 100644 --- a/loader/app/targets/empty.ts +++ b/loader/app/targets/empty.ts @@ -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(resolve => { const callback = () => { - document.removeEventListener("click", resolve); + document.removeEventListener("click", callback); resolve(); }; diff --git a/shared/js/ui/frames/SideBarRenderer.tsx b/shared/js/ui/frames/SideBarRenderer.tsx index bab53ceb..297a94e6 100644 --- a/shared/js/ui/frames/SideBarRenderer.tsx +++ b/shared/js/ui/frames/SideBarRenderer.tsx @@ -50,6 +50,7 @@ const ContentRendererServer = () => { handlerId={contentData.handlerId} messagesDeletable={true} noFirstMessageOverlay={false} + popoutable={true} /> ); }; diff --git a/shared/js/ui/frames/side/AbstractConversationController.ts b/shared/js/ui/frames/side/AbstractConversationController.ts index e7827452..4786e87b 100644 --- a/shared/js/ui/frames/side/AbstractConversationController.ts +++ b/shared/js/ui/frames/side/AbstractConversationController.ts @@ -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 | undefined | "conversation-manager-selected"; export abstract class AbstractConversationController< Events extends AbstractConversationUiEvents, Manager extends AbstractChatManager, @@ -20,15 +23,17 @@ export abstract class AbstractConversationController< > { protected readonly uiEvents: Registry; protected conversationManager: Manager | undefined; - protected listenerManager: (() => void)[]; + private listenerManager: (() => void)[]; - protected currentSelectedConversation: ConversationType; - protected currentSelectedListener: (() => void)[]; + private selectedConversation: SelectedConversation; + private currentSelectedConversation: ConversationType; + private currentSelectedListener: (() => void)[]; protected constructor() { this.uiEvents = new Registry(); 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) { + 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, timestamp: number, enforce?: boolean) { const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()] || (this.conversationManager.historyUiStates[conversation.getChatId()] = { executingUIHistoryQuery: false, diff --git a/shared/js/ui/frames/side/AbstractConversationDefinitions.ts b/shared/js/ui/frames/side/AbstractConversationDefinitions.ts index 5daf8b25..748a88f6 100644 --- a/shared/js/ui/frames/side/AbstractConversationDefinitions.ts +++ b/shared/js/ui/frames/side/AbstractConversationDefinitions.ts @@ -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 */ diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.scss b/shared/js/ui/frames/side/AbstractConversationRenderer.scss index 0f631538..5da96441 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.scss +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.scss @@ -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; diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx index 114effb5..9af95895 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx @@ -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 { - 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("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, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => { +export const ConversationPanel = React.memo((props: { + events: Registry, + 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 props.events.fire("action_self_typing", { chatId: currentChat.current.id })); }); - return
- - props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) } - /> -
+ return ( + props.events.fire("action_popout_chat")} + detachText={useTr("Open in a new window")} + className={cssStyle.panel} + > + + props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) } + /> + + ) }); diff --git a/shared/js/ui/frames/side/ChannelBarRenderer.tsx b/shared/js/ui/frames/side/ChannelBarRenderer.tsx index 62f9193f..9103c6ce 100644 --- a/shared/js/ui/frames/side/ChannelBarRenderer.tsx +++ b/shared/js/ui/frames/side/ChannelBarRenderer.tsx @@ -58,6 +58,7 @@ const ModeRendererConversation = React.memo(() => { handlerId={channelContext.handlerId} messagesDeletable={true} noFirstMessageOverlay={false} + popoutable={true} /> ); }); diff --git a/shared/js/ui/frames/side/ChannelConversationController.ts b/shared/js/ui/frames/side/ChannelConversationController.ts index fec8547e..b8c5b6e4 100644 --- a/shared/js/ui/frames/side/ChannelConversationController.ts +++ b/shared/js/ui/frames/side/ChannelConversationController.ts @@ -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) { + super.setSelectedConversation(conversation); + } + @EventHandler("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; } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelConversationDefinitions.ts b/shared/js/ui/frames/side/ChannelConversationDefinitions.ts index 7d7908e6..f424b0f6 100644 --- a/shared/js/ui/frames/side/ChannelConversationDefinitions.ts +++ b/shared/js/ui/frames/side/ChannelConversationDefinitions.ts @@ -1,3 +1,3 @@ import {AbstractConversationUiEvents} from "tc-shared/ui/frames/side/AbstractConversationDefinitions"; -export interface ChannelConversationUiEvents extends AbstractConversationUiEvents {} \ No newline at end of file +export interface ChannelConversationUiEvents extends AbstractConversationUiEvents { } \ No newline at end of file diff --git a/shared/js/ui/frames/side/PopoutConversationRenderer.tsx b/shared/js/ui/frames/side/PopoutConversationRenderer.tsx index b98b92ef..55d7948e 100644 --- a/shared/js/ui/frames/side/PopoutConversationRenderer.tsx +++ b/shared/js/ui/frames/side/PopoutConversationRenderer.tsx @@ -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() { diff --git a/shared/js/ui/frames/side/PrivateConversationController.ts b/shared/js/ui/frames/side/PrivateConversationController.ts index 776d69fb..6295f147 100644 --- a/shared/js/ui/frames/side/PrivateConversationController.ts +++ b/shared/js/ui/frames/side/PrivateConversationController.ts @@ -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() { diff --git a/shared/js/ui/frames/side/PrivateConversationRenderer.tsx b/shared/js/ui/frames/side/PrivateConversationRenderer.tsx index d329dcb6..3fbfcb4b 100644 --- a/shared/js/ui/frames/side/PrivateConversationRenderer.tsx +++ b/shared/js/ui/frames/side/PrivateConversationRenderer.tsx @@ -220,7 +220,7 @@ export const PrivateConversationsPanel = (props: { events: Registry - + diff --git a/shared/js/ui/modal/channel-chat/Controller.ts b/shared/js/ui/modal/channel-chat/Controller.ts new file mode 100644 index 00000000..e370d6f3 --- /dev/null +++ b/shared/js/ui/modal/channel-chat/Controller.ts @@ -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()); +} \ No newline at end of file diff --git a/shared/js/ui/modal/channel-chat/Definitions.ts b/shared/js/ui/modal/channel-chat/Definitions.ts new file mode 100644 index 00000000..d276188a --- /dev/null +++ b/shared/js/ui/modal/channel-chat/Definitions.ts @@ -0,0 +1,9 @@ +import {IpcRegistryDescription} from "tc-events"; +import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions"; + +export interface ModalChannelChatParameters { + events: IpcRegistryDescription, + channelName: string, + channelId: number, + handlerId: string +} \ No newline at end of file diff --git a/shared/js/ui/modal/channel-chat/Renderer.scss b/shared/js/ui/modal/channel-chat/Renderer.scss new file mode 100644 index 00000000..06a50a29 --- /dev/null +++ b/shared/js/ui/modal/channel-chat/Renderer.scss @@ -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%; + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/channel-chat/Renderer.tsx b/shared/js/ui/modal/channel-chat/Renderer.tsx new file mode 100644 index 00000000..7cd44bdb --- /dev/null +++ b/shared/js/ui/modal/channel-chat/Renderer.tsx @@ -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; + + constructor(parameters: ModalChannelChatParameters) { + super(); + + this.parameters = parameters; + this.events = Registry.fromIpcDescription(parameters.events); + } + + protected onDestroy() { + super.onDestroy(); + + this.events.destroy(); + } + + renderBody(): React.ReactElement { + return ( +
+ +
+ ); + } + + renderTitle(): string | React.ReactElement { + return ( + + + + ); + } + +} + +export default Modal; \ No newline at end of file diff --git a/shared/js/ui/react-elements/DetachButton.tsx b/shared/js/ui/react-elements/DetachButton.tsx new file mode 100644 index 00000000..0c6e063e --- /dev/null +++ b/shared/js/ui/react-elements/DetachButton.tsx @@ -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 ( +
+
+
+ +
+
+ {props.children} +
+ ); +}); \ No newline at end of file diff --git a/shared/js/ui/react-elements/DetachButtons.scss b/shared/js/ui/react-elements/DetachButtons.scss new file mode 100644 index 00000000..9986ec76 --- /dev/null +++ b/shared/js/ui/react-elements/DetachButtons.scss @@ -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; + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Definitions.ts b/shared/js/ui/react-elements/modal/Definitions.ts index 7003f5a7..8e4a04e2 100644 --- a/shared/js/ui/react-elements/modal/Definitions.ts +++ b/shared/js/ui/react-elements/modal/Definitions.ts @@ -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, /* variables */ IpcVariableDescriptor ], + "channel-chat": [ + /* parameters */ ModalChannelChatParameters + ], "echo-test": [ /* events */ IpcRegistryDescription ], diff --git a/shared/js/ui/react-elements/modal/Registry.ts b/shared/js/ui/react-elements/modal/Registry.ts index 2c6306d4..74c924a8 100644 --- a/shared/js/ui/react-elements/modal/Registry.ts +++ b/shared/js/ui/react-elements/modal/Registry.ts @@ -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"), diff --git a/shared/js/ui/tree/EntryTags.tsx b/shared/js/ui/tree/EntryTags.tsx index 26bbe47e..b9ca49d6 100644 --- a/shared/js/ui/tree/EntryTags.tsx +++ b/shared/js/ui/tree/EntryTags.tsx @@ -106,38 +106,45 @@ export const ChannelTag = React.memo((props: { channelName: string, channelId: number, handlerId: string, - className?: string -}) => ( -
{ - event.preventDefault(); + className?: string, - ipcChannel.sendMessage("contextmenu-channel", { - handlerId: props.handlerId, - channelId: props.channelId, + style?: EntryTagStyle +}) => { + if(props.style === "text-only") { + return {props.channelName}; + } + return ( +
{ + 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} -
-)); + 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} +
+ ); +}); loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "entry tags", diff --git a/web/app/audio/Recorder.ts b/web/app/audio/Recorder.ts index 85e92f54..c75cd7c3 100644 --- a/web/app/audio/Recorder.ts +++ b/web/app/audio/Recorder.ts @@ -535,7 +535,7 @@ class JavascriptLevelMeter implements LevelMeter { async initialize() { try { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const timeout = setTimeout(reject, 5000); getAudioBackend().executeWhenInitialized(() => { clearTimeout(timeout); diff --git a/web/app/connection/ServerConnection.ts b/web/app/connection/ServerConnection.ts index 6fe2f4a2..05ca9719 100644 --- a/web/app/connection/ServerConnection.ts +++ b/web/app/connection/ServerConnection.ts @@ -160,13 +160,13 @@ export class ServerConnection extends AbstractServerConnection { })); let timeoutRaised = false; - let timeoutPromise = new Promise(resolve => setTimeout(() => { + let timeoutPromise = new Promise(resolve => setTimeout(() => { timeoutRaised = true; resolve(); }, timeout)); let cancelRaised = false; - let cancelPromise = new Promise(resolve => { + let cancelPromise = new Promise(resolve => { this.connectCancelCallback = () => { this.connectCancelCallback = undefined; cancelRaised = true;