Adding popout to channel conversations and fixed doubling of chat messages
This commit is contained in:
parent
32de3542df
commit
1cae741b17
24 changed files with 362 additions and 86 deletions
|
@ -1,4 +1,8 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **29.04.21**
|
||||||
|
- Fixed a bug which caused chat messages to appear twice
|
||||||
|
- Adding support for poping out channel conversations
|
||||||
|
|
||||||
* **27.04.21**
|
* **27.04.21**
|
||||||
- Implemented support for showing the video feed watchers
|
- Implemented support for showing the video feed watchers
|
||||||
- Updating the channel tree if the channel client order changes
|
- Updating the channel tree if the channel client order changes
|
||||||
|
|
|
@ -11,9 +11,9 @@ export default class implements ApplicationLoader {
|
||||||
console.log("Doing nothing");
|
console.log("Doing nothing");
|
||||||
|
|
||||||
for(let index of [1, 2, 3]) {
|
for(let index of [1, 2, 3]) {
|
||||||
await new Promise(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
document.removeEventListener("click", resolve);
|
document.removeEventListener("click", callback);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ const ContentRendererServer = () => {
|
||||||
handlerId={contentData.handlerId}
|
handlerId={contentData.handlerId}
|
||||||
messagesDeletable={true}
|
messagesDeletable={true}
|
||||||
noFirstMessageOverlay={false}
|
noFirstMessageOverlay={false}
|
||||||
|
popoutable={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {AbstractConversationUiEvents, ChatHistoryState} from "./AbstractConversationDefinitions";
|
import {AbstractConversationUiEvents, ChatHistoryState} from "./AbstractConversationDefinitions";
|
||||||
import {EventHandler, Registry} from "../../../events";
|
import {EventHandler, Registry} from "tc-events";
|
||||||
import {LogCategory, logError} from "../../../log";
|
import {LogCategory, logError} from "../../../log";
|
||||||
import {tr, tra} from "../../../i18n/localize";
|
import {tr, tra} from "../../../i18n/localize";
|
||||||
import {
|
import {
|
||||||
|
@ -8,9 +8,12 @@ import {
|
||||||
AbstractChatManagerEvents,
|
AbstractChatManagerEvents,
|
||||||
AbstractConversationEvents
|
AbstractConversationEvents
|
||||||
} from "tc-shared/conversations/AbstractConversion";
|
} 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 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<
|
export abstract class AbstractConversationController<
|
||||||
Events extends AbstractConversationUiEvents,
|
Events extends AbstractConversationUiEvents,
|
||||||
Manager extends AbstractChatManager<ManagerEvents, ConversationType, ConversationEvents>,
|
Manager extends AbstractChatManager<ManagerEvents, ConversationType, ConversationEvents>,
|
||||||
|
@ -20,15 +23,17 @@ export abstract class AbstractConversationController<
|
||||||
> {
|
> {
|
||||||
protected readonly uiEvents: Registry<Events>;
|
protected readonly uiEvents: Registry<Events>;
|
||||||
protected conversationManager: Manager | undefined;
|
protected conversationManager: Manager | undefined;
|
||||||
protected listenerManager: (() => void)[];
|
private listenerManager: (() => void)[];
|
||||||
|
|
||||||
protected currentSelectedConversation: ConversationType;
|
private selectedConversation: SelectedConversation<ConversationType>;
|
||||||
protected currentSelectedListener: (() => void)[];
|
private currentSelectedConversation: ConversationType;
|
||||||
|
private currentSelectedListener: (() => void)[];
|
||||||
|
|
||||||
protected constructor() {
|
protected constructor() {
|
||||||
this.uiEvents = new Registry<Events>();
|
this.uiEvents = new Registry<Events>();
|
||||||
this.currentSelectedListener = [];
|
this.currentSelectedListener = [];
|
||||||
this.listenerManager = [];
|
this.listenerManager = [];
|
||||||
|
this.selectedConversation = "conversation-manager-selected";
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -50,27 +55,48 @@ export abstract class AbstractConversationController<
|
||||||
|
|
||||||
this.listenerManager.forEach(callback => callback());
|
this.listenerManager.forEach(callback => callback());
|
||||||
this.listenerManager = [];
|
this.listenerManager = [];
|
||||||
this.conversationManager = manager;
|
|
||||||
|
|
||||||
if(manager) {
|
this.conversationManager = manager;
|
||||||
this.registerConversationManagerEvents(manager);
|
this.selectedConversation = undefined;
|
||||||
this.setCurrentlySelected(manager.getSelectedConversation());
|
this.setCurrentlySelected(undefined);
|
||||||
} else {
|
|
||||||
this.setCurrentlySelected(undefined);
|
if(this.conversationManager) {
|
||||||
|
this.registerConversationManagerEvents(this.conversationManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerConversationManagerEvents(manager: Manager) {
|
protected setSelectedConversation(conversation: SelectedConversation<ConversationType>) {
|
||||||
this.listenerManager.push(manager.events.on("notify_selected_changed", event => this.setCurrentlySelected(event.newConversation)));
|
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", () => {
|
this.listenerManager.push(manager.events.on("notify_cross_conversation_support_changed", () => {
|
||||||
const currentConversation = this.getCurrentConversation();
|
const currentConversation = this.getCurrentConversation();
|
||||||
if(currentConversation) {
|
if(currentConversation) {
|
||||||
this.reportStateToUI(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.currentSelectedListener.push(conversation.events.on("notify_unread_timestamp_changed", event =>
|
||||||
this.uiEvents.fire_react("notify_unread_timestamp_changed", { chatId: conversation.getChatId(), timestamp: event.timestamp })));
|
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.currentSelectedListener.push(conversation.events.on("notify_read_state_changed", () => {
|
||||||
this.reportStateToUI(conversation);
|
this.reportStateToUI(conversation);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return this.currentSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setCurrentlySelected(conversation: ConversationType | undefined) {
|
private setCurrentlySelected(conversation: ConversationType | undefined) {
|
||||||
if(this.currentSelectedConversation === conversation) {
|
if(this.currentSelectedConversation === conversation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -182,6 +210,7 @@ export abstract class AbstractConversationController<
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public uiQueryHistory(conversation: AbstractChat<any>, timestamp: number, enforce?: boolean) {
|
public uiQueryHistory(conversation: AbstractChat<any>, timestamp: number, enforce?: boolean) {
|
||||||
const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()] || (this.conversationManager.historyUiStates[conversation.getChatId()] = {
|
const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()] || (this.conversationManager.historyUiStates[conversation.getChatId()] = {
|
||||||
executingUIHistoryQuery: false,
|
executingUIHistoryQuery: false,
|
||||||
|
|
|
@ -119,6 +119,7 @@ export interface AbstractConversationUiEvents {
|
||||||
action_send_message: { text: string, chatId: string },
|
action_send_message: { text: string, chatId: string },
|
||||||
action_jump_to_present: { chatId: string },
|
action_jump_to_present: { chatId: string },
|
||||||
action_focus_chat: {},
|
action_focus_chat: {},
|
||||||
|
action_popout_chat: {},
|
||||||
|
|
||||||
query_selected_chat: {},
|
query_selected_chat: {},
|
||||||
/* will cause a notify_selected_chat */
|
/* will cause a notify_selected_chat */
|
||||||
|
|
|
@ -52,6 +52,11 @@ html:root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
|
min-height: 10em;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
background: var(--chat-background);
|
background: var(--chat-background);
|
||||||
|
|
||||||
position: relative;
|
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 {ColloquialFormat, date_format, format_date_general, formatDayTime} from "tc-shared/utils/DateUtils";
|
||||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||||
import {ChatBox} from "tc-shared/ui/react-elements/ChatBox";
|
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");
|
const cssStyle = require("./AbstractConversationRenderer.scss");
|
||||||
|
|
||||||
|
@ -319,12 +321,14 @@ const PartnerTypingIndicator = (props: { events: Registry<AbstractConversationUi
|
||||||
});
|
});
|
||||||
|
|
||||||
props.events.reactUse("notify_chat_event", event => {
|
props.events.reactUse("notify_chat_event", event => {
|
||||||
if(event.chatId !== props.chatId)
|
if(event.chatId !== props.chatId) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(event.event.type === "message") {
|
if(event.event.type === "message") {
|
||||||
if(!event.event.isOwnMessage)
|
if(!event.event.isOwnMessage) {
|
||||||
setTypingTimestamp(0);
|
setTypingTimestamp(0);
|
||||||
|
}
|
||||||
} else if(event.event.type === "partner-action" || event.event.type === "local-action") {
|
} else if(event.event.type === "partner-action" || event.event.type === "local-action") {
|
||||||
setTypingTimestamp(0);
|
setTypingTimestamp(0);
|
||||||
}
|
}
|
||||||
|
@ -774,11 +778,13 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
|
||||||
|
|
||||||
@EventHandler<AbstractConversationUiEvents>("notify_chat_event")
|
@EventHandler<AbstractConversationUiEvents>("notify_chat_event")
|
||||||
private handleChatEvent(event: 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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(event.event.type === "local-user-switch" && !this.showSwitchEvents)
|
if(event.event.type === "local-user-switch" && !this.showSwitchEvents) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.chatEvents.push(event.event);
|
this.chatEvents.push(event.event);
|
||||||
this.sortEvents();
|
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 currentChat = useRef({ id: "unselected" });
|
||||||
const chatEnabled = useRef(false);
|
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 refChatBox.current.events.on("notify_typing", () => props.events.fire("action_self_typing", { chatId: currentChat.current.id }));
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={cssStyle.panel}>
|
return (
|
||||||
<ConversationMessages events={props.events} handlerId={props.handlerId} messagesDeletable={props.messagesDeletable} noFirstMessageOverlay={props.noFirstMessageOverlay} />
|
<DetachButton
|
||||||
<ChatBox
|
disabled={!props.popoutable}
|
||||||
ref={refChatBox}
|
detached={false}
|
||||||
onSubmit={text => props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) }
|
callbackToggle={() => props.events.fire("action_popout_chat")}
|
||||||
/>
|
detachText={useTr("Open in a new window")}
|
||||||
</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 }) }
|
||||||
|
/>
|
||||||
|
</DetachButton>
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,6 +58,7 @@ const ModeRendererConversation = React.memo(() => {
|
||||||
handlerId={channelContext.handlerId}
|
handlerId={channelContext.handlerId}
|
||||||
messagesDeletable={true}
|
messagesDeletable={true}
|
||||||
noFirstMessageOverlay={false}
|
noFirstMessageOverlay={false}
|
||||||
|
popoutable={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {EventHandler} from "tc-events";
|
||||||
import {LogCategory, logError} from "../../../log";
|
import {LogCategory, logError} from "../../../log";
|
||||||
import {tr} from "../../../i18n/localize";
|
import {tr} from "../../../i18n/localize";
|
||||||
import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions";
|
import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions";
|
||||||
import {AbstractConversationController} from "./AbstractConversationController";
|
import {AbstractConversationController, SelectedConversation} from "./AbstractConversationController";
|
||||||
import {
|
import {
|
||||||
ChannelConversation,
|
ChannelConversation,
|
||||||
ChannelConversationEvents,
|
ChannelConversationEvents,
|
||||||
|
@ -11,6 +11,7 @@ import {
|
||||||
ChannelConversationManagerEvents
|
ChannelConversationManagerEvents
|
||||||
} from "tc-shared/conversations/ChannelConversationManager";
|
} from "tc-shared/conversations/ChannelConversationManager";
|
||||||
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
|
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
|
||||||
|
import {spawnModalChannelChat} from "tc-shared/ui/modal/channel-chat/Controller";
|
||||||
|
|
||||||
export class ChannelConversationController extends AbstractConversationController<
|
export class ChannelConversationController extends AbstractConversationController<
|
||||||
ChannelConversationUiEvents,
|
ChannelConversationUiEvents,
|
||||||
|
@ -26,17 +27,15 @@ export class ChannelConversationController extends AbstractConversationControlle
|
||||||
super();
|
super();
|
||||||
this.connectionListener = [];
|
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.registerHandler(this, true);
|
||||||
|
this.uiEvents.on("action_popout_chat", () => {
|
||||||
|
const conversation = this.getCurrentConversation();
|
||||||
|
if(!conversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnModalChannelChat(this.connection, conversation);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -59,11 +58,16 @@ export class ChannelConversationController extends AbstractConversationControlle
|
||||||
if(connection) {
|
if(connection) {
|
||||||
/* FIXME: Update cross channel talk state! */
|
/* FIXME: Update cross channel talk state! */
|
||||||
this.setConversationManager(connection.getChannelConversations());
|
this.setConversationManager(connection.getChannelConversations());
|
||||||
|
this.setSelectedConversation("conversation-manager-selected");
|
||||||
} else {
|
} else {
|
||||||
this.setConversationManager(undefined);
|
this.setConversationManager(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedConversation(conversation: SelectedConversation<ChannelConversation>) {
|
||||||
|
super.setSelectedConversation(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler<AbstractConversationUiEvents>("action_delete_message")
|
@EventHandler<AbstractConversationUiEvents>("action_delete_message")
|
||||||
private handleMessageDelete(event: AbstractConversationUiEvents["action_delete_message"]) {
|
private handleMessageDelete(event: AbstractConversationUiEvents["action_delete_message"]) {
|
||||||
const conversation = this.conversationManager?.findConversationById(event.chatId);
|
const conversation = this.conversationManager?.findConversationById(event.chatId);
|
||||||
|
@ -75,15 +79,17 @@ export class ChannelConversationController extends AbstractConversationControlle
|
||||||
conversation.deleteMessage(event.uniqueId);
|
conversation.deleteMessage(event.uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerConversationEvents(conversation: ChannelConversation) {
|
protected registerConversationEvents(conversation: ChannelConversation): (() => void)[] {
|
||||||
super.registerConversationEvents(conversation);
|
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.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);
|
this.reportStateToUI(conversation);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return events;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
import {AbstractConversationUiEvents} from "tc-shared/ui/frames/side/AbstractConversationDefinitions";
|
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}
|
handlerId={this.userData.handlerId}
|
||||||
events={this.events}
|
events={this.events}
|
||||||
messagesDeletable={this.userData.messagesDeletable}
|
messagesDeletable={this.userData.messagesDeletable}
|
||||||
noFirstMessageOverlay={this.userData.noFirstMessageOverlay} />;
|
noFirstMessageOverlay={this.userData.noFirstMessageOverlay}
|
||||||
|
popoutable={false}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTitle() {
|
renderTitle() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||||
import {EventHandler} from "../../../events";
|
import {EventHandler} from "tc-events";
|
||||||
import {
|
import {
|
||||||
PrivateConversationInfo,
|
PrivateConversationInfo,
|
||||||
PrivateConversationUIEvents
|
PrivateConversationUIEvents
|
||||||
|
@ -74,16 +74,17 @@ export class PrivateConversationController extends AbstractConversationControlle
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
if(connection) {
|
if(connection) {
|
||||||
this.setConversationManager(connection.getPrivateConversations());
|
this.setConversationManager(connection.getPrivateConversations());
|
||||||
|
this.setSelectedConversation("conversation-manager-selected");
|
||||||
} else {
|
} else {
|
||||||
this.setConversationManager(undefined);
|
this.setConversationManager(undefined);
|
||||||
}
|
}
|
||||||
this.reportConversationList();
|
this.reportConversationList();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerConversationManagerEvents(manager: PrivateConversationManager) {
|
protected registerConversationManagerEvents(manager: PrivateConversationManager): (() => void)[] {
|
||||||
super.registerConversationManagerEvents(manager);
|
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 conversation = event.conversation;
|
||||||
const events = this.listenerConversation[conversation.getChatId()] = [];
|
const events = this.listenerConversation[conversation.getChatId()] = [];
|
||||||
events.push(conversation.events.on("notify_partner_changed", event => {
|
events.push(conversation.events.on("notify_partner_changed", event => {
|
||||||
|
@ -101,17 +102,18 @@ export class PrivateConversationController extends AbstractConversationControlle
|
||||||
|
|
||||||
this.reportConversationList();
|
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());
|
this.listenerConversation[event.conversation.getChatId()]?.forEach(callback => callback());
|
||||||
delete this.listenerConversation[event.conversation.getChatId()];
|
delete this.listenerConversation[event.conversation.getChatId()];
|
||||||
|
|
||||||
this.reportConversationList();
|
this.reportConversationList();
|
||||||
}));
|
}));
|
||||||
this.listenerManager.push(manager.events.on("notify_selected_changed", () => this.reportConversationList()));
|
events.push(manager.events.on("notify_selected_changed", () => this.reportConversationList()));
|
||||||
this.listenerManager.push(() => {
|
events.push(() => {
|
||||||
Object.values(this.listenerConversation).forEach(callbacks => callbacks.forEach(callback => callback()));
|
Object.values(this.listenerConversation).forEach(callbacks => callbacks.forEach(callback => callback()));
|
||||||
this.listenerConversation = {};
|
this.listenerConversation = {};
|
||||||
});
|
});
|
||||||
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInput() {
|
focusInput() {
|
||||||
|
|
|
@ -220,7 +220,7 @@ export const PrivateConversationsPanel = (props: { events: Registry<PrivateConve
|
||||||
<div className={cssStyle.dividerContainer}>
|
<div className={cssStyle.dividerContainer}>
|
||||||
<OpenConversationsPanel />
|
<OpenConversationsPanel />
|
||||||
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider} />
|
<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>
|
</div>
|
||||||
</EventContext.Provider>
|
</EventContext.Provider>
|
||||||
</HandlerIdContext.Provider>
|
</HandlerIdContext.Provider>
|
||||||
|
|
27
shared/js/ui/modal/channel-chat/Controller.ts
Normal file
27
shared/js/ui/modal/channel-chat/Controller.ts
Normal 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());
|
||||||
|
}
|
9
shared/js/ui/modal/channel-chat/Definitions.ts
Normal file
9
shared/js/ui/modal/channel-chat/Definitions.ts
Normal 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
|
||||||
|
}
|
20
shared/js/ui/modal/channel-chat/Renderer.scss
Normal file
20
shared/js/ui/modal/channel-chat/Renderer.scss
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
58
shared/js/ui/modal/channel-chat/Renderer.tsx
Normal file
58
shared/js/ui/modal/channel-chat/Renderer.tsx
Normal 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;
|
28
shared/js/ui/react-elements/DetachButton.tsx
Normal file
28
shared/js/ui/react-elements/DetachButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
46
shared/js/ui/react-elements/DetachButtons.scss
Normal file
46
shared/js/ui/react-elements/DetachButtons.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
|
||||||
import {ModalChannelInfoEvents, ModalChannelInfoVariables} from "tc-shared/ui/modal/channel-info/Definitions";
|
import {ModalChannelInfoEvents, ModalChannelInfoVariables} from "tc-shared/ui/modal/channel-info/Definitions";
|
||||||
import {ModalVideoViewersEvents, ModalVideoViewersVariables} from "tc-shared/ui/modal/video-viewers/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 ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -185,6 +186,9 @@ export interface ModalConstructorArguments {
|
||||||
/* events */ IpcRegistryDescription<ModalChannelInfoEvents>,
|
/* events */ IpcRegistryDescription<ModalChannelInfoEvents>,
|
||||||
/* variables */ IpcVariableDescriptor<ModalChannelInfoVariables>
|
/* variables */ IpcVariableDescriptor<ModalChannelInfoVariables>
|
||||||
],
|
],
|
||||||
|
"channel-chat": [
|
||||||
|
/* parameters */ ModalChannelChatParameters
|
||||||
|
],
|
||||||
"echo-test": [
|
"echo-test": [
|
||||||
/* events */ IpcRegistryDescription<EchoTestEvents>
|
/* events */ IpcRegistryDescription<EchoTestEvents>
|
||||||
],
|
],
|
||||||
|
|
|
@ -43,6 +43,12 @@ registerModal({
|
||||||
popoutSupported: true
|
popoutSupported: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerModal({
|
||||||
|
modalId: "channel-chat",
|
||||||
|
classLoader: async () => await import("tc-shared/ui/modal/channel-chat/Renderer"),
|
||||||
|
popoutSupported: true
|
||||||
|
});
|
||||||
|
|
||||||
registerModal({
|
registerModal({
|
||||||
modalId: "echo-test",
|
modalId: "echo-test",
|
||||||
classLoader: async () => await import("tc-shared/ui/modal/echo-test/Renderer"),
|
classLoader: async () => await import("tc-shared/ui/modal/echo-test/Renderer"),
|
||||||
|
|
|
@ -106,38 +106,45 @@ export const ChannelTag = React.memo((props: {
|
||||||
channelName: string,
|
channelName: string,
|
||||||
channelId: number,
|
channelId: number,
|
||||||
handlerId: string,
|
handlerId: string,
|
||||||
className?: string
|
className?: string,
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
|
||||||
onContextMenu={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
ipcChannel.sendMessage("contextmenu-channel", {
|
style?: EntryTagStyle
|
||||||
handlerId: props.handlerId,
|
}) => {
|
||||||
channelId: props.channelId,
|
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,
|
ipcChannel.sendMessage("contextmenu-channel", {
|
||||||
pageY: event.pageY
|
handlerId: props.handlerId,
|
||||||
});
|
channelId: props.channelId,
|
||||||
}}
|
|
||||||
draggable={true}
|
pageX: event.pageX,
|
||||||
onDragStart={event => {
|
pageY: event.pageY
|
||||||
event.dataTransfer.effectAllowed = "all";
|
});
|
||||||
event.dataTransfer.dropEffect = "move";
|
}}
|
||||||
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.ChannelGreen, name: props.channelName }]), 0, 6);
|
draggable={true}
|
||||||
setupDragData(event.dataTransfer, props.handlerId, [
|
onDragStart={event => {
|
||||||
{
|
event.dataTransfer.effectAllowed = "all";
|
||||||
type: "channel",
|
event.dataTransfer.dropEffect = "move";
|
||||||
channelId: props.channelId
|
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.ChannelGreen, name: props.channelName }]), 0, 6);
|
||||||
}
|
setupDragData(event.dataTransfer, props.handlerId, [
|
||||||
], "channel");
|
{
|
||||||
event.dataTransfer.setData("text/plain", props.channelName);
|
type: "channel",
|
||||||
}}
|
channelId: props.channelId
|
||||||
>
|
}
|
||||||
{props.channelName}
|
], "channel");
|
||||||
</div>
|
event.dataTransfer.setData("text/plain", props.channelName);
|
||||||
));
|
}}
|
||||||
|
>
|
||||||
|
{props.channelName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||||
name: "entry tags",
|
name: "entry tags",
|
||||||
|
|
|
@ -535,7 +535,7 @@ class JavascriptLevelMeter implements LevelMeter {
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const timeout = setTimeout(reject, 5000);
|
const timeout = setTimeout(reject, 5000);
|
||||||
getAudioBackend().executeWhenInitialized(() => {
|
getAudioBackend().executeWhenInitialized(() => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
|
@ -160,13 +160,13 @@ export class ServerConnection extends AbstractServerConnection {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let timeoutRaised = false;
|
let timeoutRaised = false;
|
||||||
let timeoutPromise = new Promise(resolve => setTimeout(() => {
|
let timeoutPromise = new Promise<void>(resolve => setTimeout(() => {
|
||||||
timeoutRaised = true;
|
timeoutRaised = true;
|
||||||
resolve();
|
resolve();
|
||||||
}, timeout));
|
}, timeout));
|
||||||
|
|
||||||
let cancelRaised = false;
|
let cancelRaised = false;
|
||||||
let cancelPromise = new Promise(resolve => {
|
let cancelPromise = new Promise<void>(resolve => {
|
||||||
this.connectCancelCallback = () => {
|
this.connectCancelCallback = () => {
|
||||||
this.connectCancelCallback = undefined;
|
this.connectCancelCallback = undefined;
|
||||||
cancelRaised = true;
|
cancelRaised = true;
|
||||||
|
|
Loading…
Add table
Reference in a new issue