Adding popout to channel conversations and fixed doubling of chat messages
parent
32de3542df
commit
1cae741b17
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ const ContentRendererServer = () => {
|
|||
handlerId={contentData.handlerId}
|
||||
messagesDeletable={true}
|
||||
noFirstMessageOverlay={false}
|
||||
popoutable={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
});
|
||||
|
|
|
@ -58,6 +58,7 @@ const ModeRendererConversation = React.memo(() => {
|
|||
handlerId={channelContext.handlerId}
|
||||
messagesDeletable={true}
|
||||
noFirstMessageOverlay={false}
|
||||
popoutable={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
import {AbstractConversationUiEvents} from "tc-shared/ui/frames/side/AbstractConversationDefinitions";
|
||||
|
||||
export interface ChannelConversationUiEvents extends AbstractConversationUiEvents {}
|
||||
export interface ChannelConversationUiEvents extends AbstractConversationUiEvents { }
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
],
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue