diff --git a/ChangeLog.md b/ChangeLog.md index e46cbd86..9a39b3f2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,7 @@ - Properly updating the private message unread count - Improved channel conversation mode detection support - Added support for HTML encoded links (Example would be when copying from Edge the URL) + - Enabled context menus for all clickable client tags * **08.12.20** - Fixed the permission editor not resolving unique ids diff --git a/shared/js/conversations/ChannelConversationManager.ts b/shared/js/conversations/ChannelConversationManager.ts index 103d6b6a..1c2f08d2 100644 --- a/shared/js/conversations/ChannelConversationManager.ts +++ b/shared/js/conversations/ChannelConversationManager.ts @@ -398,7 +398,7 @@ export class ChannelConversationManager extends AbstractChatManager this.handleHandlerCreated(event.handler)); diff --git a/shared/js/file/LocalIcons.ts b/shared/js/file/LocalIcons.ts index 7157b4cf..35d95211 100644 --- a/shared/js/file/LocalIcons.ts +++ b/shared/js/file/LocalIcons.ts @@ -69,7 +69,7 @@ class IconManager extends AbstractIconManager { constructor() { super(); - this.ipcChannel = ipc.getInstance().createChannel(undefined, kIPCIconChannel); + this.ipcChannel = ipc.getIpcInstance().createChannel(undefined, kIPCIconChannel); this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); server_connections.events().on("notify_handler_created", event => { diff --git a/shared/js/file/RemoteAvatars.ts b/shared/js/file/RemoteAvatars.ts index e319735f..ba11bd3f 100644 --- a/shared/js/file/RemoteAvatars.ts +++ b/shared/js/file/RemoteAvatars.ts @@ -159,7 +159,7 @@ class RemoteAvatarManagerFactory extends AbstractAvatarManagerFactory { constructor() { super(); - this.ipcChannel = ipc.getInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCAvatarChannel); + this.ipcChannel = ipc.getIpcInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCAvatarChannel); this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); } diff --git a/shared/js/file/RemoteIcons.ts b/shared/js/file/RemoteIcons.ts index 2ba3eb0e..9f725bfe 100644 --- a/shared/js/file/RemoteIcons.ts +++ b/shared/js/file/RemoteIcons.ts @@ -33,7 +33,7 @@ class RemoteIconManager extends AbstractIconManager { constructor() { super(); - this.ipcChannel = ipc.getInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCIconChannel); + this.ipcChannel = ipc.getIpcInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCIconChannel); this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); } diff --git a/shared/js/ipc/BrowserIPC.ts b/shared/js/ipc/BrowserIPC.ts index cc192f6d..32c67148 100644 --- a/shared/js/ipc/BrowserIPC.ts +++ b/shared/js/ipc/BrowserIPC.ts @@ -49,16 +49,16 @@ export abstract class BasicIPCHandler { protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000"; protected static readonly PROTOCOL_VERSION = 1; - protected _channels: IPCChannel[] = []; - protected unique_id; + protected registeredChannels: IPCChannel[] = []; + protected localUniqueId: string; protected constructor() { } setup() { - this.unique_id = uuidv4(); /* lets get an unique identifier */ + this.localUniqueId = uuidv4(); } - getLocalAddress() { return this.unique_id; } + getLocalAddress() : string { return this.localUniqueId; } abstract sendMessage(type: string, data: any, target?: string); @@ -72,12 +72,12 @@ export abstract class BasicIPCHandler { request_query_id: (message.data).query_id, request_timestamp: (message.data).timestamp, - device_id: this.unique_id, + device_id: this.localUniqueId, protocol: BasicIPCHandler.PROTOCOL_VERSION } as ProcessQueryResponse, message.sender); return; } - } else if(message.receiver === this.unique_id) { + } else if(message.receiver === this.localUniqueId) { if(message.type == "process-query-response") { const response: ProcessQueryResponse = message.data; if(this._query_results[response.request_query_id]) @@ -114,7 +114,7 @@ export abstract class BasicIPCHandler { const data: ChannelMessage = message.data; let channel_invoked = false; - for(const channel of this._channels) { + for(const channel of this.registeredChannels) { if(channel.channelId === data.channel_id && (typeof(channel.targetClientId) === "undefined" || channel.targetClientId === message.sender)) { if(channel.messageHandler) channel.messageHandler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data); @@ -136,8 +136,9 @@ export abstract class BasicIPCHandler { messageHandler: undefined, sendMessage: (type: string, data: any, target?: string) => { if(typeof target !== "undefined") { - if(typeof channel.targetClientId === "string" && target != channel.targetClientId) + if(typeof channel.targetClientId === "string" && target != channel.targetClientId) { throw "target id does not match channel target"; + } } this.sendMessage("channel", { @@ -148,14 +149,14 @@ export abstract class BasicIPCHandler { } }; - this._channels.push(channel); + this.registeredChannels.push(channel); return channel; } - channels() : IPCChannel[] { return this._channels; } + channels() : IPCChannel[] { return this.registeredChannels; } deleteChannel(channel: IPCChannel) { - this._channels = this._channels.filter(e => e !== channel); + this.registeredChannels = this.registeredChannels.filter(e => e !== channel); } private _query_results: {[key: string]:ProcessQueryResponse[]} = {}; @@ -178,7 +179,7 @@ export abstract class BasicIPCHandler { register_certificate_accept_callback(callback: () => any) : string { const id = uuidv4(); this._cert_accept_callbacks[id] = callback; - return this.unique_id + ":" + id; + return this.localUniqueId + ":" + id; } private _cert_accept_succeeded: {[sender: string]:(() => any)} = {}; @@ -250,13 +251,17 @@ class BroadcastChannelIPC extends BasicIPCHandler { sendMessage(type: string, data: any, target?: string) { const message: BroadcastMessage = {} as any; - message.sender = this.unique_id; + message.sender = this.localUniqueId; message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID; message.timestamp = Date.now(); message.type = type; message.data = data; - this.channel.postMessage(JSON.stringify(message)); + if(message.receiver === this.localUniqueId) { + this.handleMessage(message); + } else { + this.channel.postMessage(JSON.stringify(message)); + } } } @@ -277,7 +282,7 @@ export function setup() { connect_handler.setup(); } -export function getInstance() { +export function getIpcInstance() { return handler; } diff --git a/shared/js/text/markdown.ts b/shared/js/text/markdown.ts index 4660c957..e75cc7f6 100644 --- a/shared/js/text/markdown.ts +++ b/shared/js/text/markdown.ts @@ -14,7 +14,6 @@ import { TextToken, Token } from "remarkable/lib"; -import {escapeBBCode} from "../text/bbcode"; import {tr} from "tc-shared/i18n/localize"; const { Remarkable } = require("remarkable"); @@ -29,7 +28,7 @@ export class MD2BBCodeRenderer { "hardbreak": () => "\n", "paragraph_open": () => "", - "paragraph_close": (_, token: ParagraphCloseToken) => token.tight ? "" : "[br]", + "paragraph_close": (_, token: ParagraphCloseToken) => token.tight ? "" : "\n", "strong_open": () => "[b]", "strong_close": () => "[/b]", @@ -119,7 +118,7 @@ export class MD2BBCodeRenderer { if(tokens[index].lines?.length) { while(this.currentLineCount < tokens[index].lines[0]) { this.currentLineCount += 1; - result += "[br]"; + result += "\n"; } } diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index bc83a999..67e8dd97 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -26,6 +26,8 @@ import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller"; import {Settings, settings} from "tc-shared/settings"; import {ClientIcon} from "svg-sprites/client-icons"; +import "./EntryTagsHandler"; + export interface ChannelTreeEvents { /* general tree notified */ notify_tree_reset: {}, diff --git a/shared/js/tree/EntryTagsHandler.ts b/shared/js/tree/EntryTagsHandler.ts new file mode 100644 index 00000000..54f26673 --- /dev/null +++ b/shared/js/tree/EntryTagsHandler.ts @@ -0,0 +1,101 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {getIpcInstance} from "tc-shared/ipc/BrowserIPC"; +import {LogCategory, logWarn} from "tc-shared/log"; +import {server_connections} from "tc-shared/ConnectionManager"; + +const kIpcChannel = "entry-tags"; + +function handleIpcMessage(type: string, payload: any) { + switch (type) { + case "contextmenu-client": { + const { + handlerId, + + clientUniqueId, + clientId, + clientDatabaseId, + + pageX, + pageY + } = payload; + + if(typeof pageX !== "number" || typeof pageY !== "number") { + logWarn(LogCategory.IPC, tr("Received client context menu action with an invalid page coordinated: %ox%o."), pageX, pageY); + return; + } + + if(typeof handlerId !== "string") { + logWarn(LogCategory.IPC, tr("Received client context menu action with an invalid handler id: %o."), handlerId); + return; + } + + if(typeof clientUniqueId !== "string") { + logWarn(LogCategory.IPC, tr("Received client context menu action with an invalid client unique id: %o."), clientUniqueId); + return; + } + + if(clientId !== undefined && typeof clientId !== "number") { + logWarn(LogCategory.IPC, tr("Received client context menu action with an invalid client id: %o."), clientId); + return; + } + + if(clientDatabaseId !== undefined && typeof clientDatabaseId !== "number") { + logWarn(LogCategory.IPC, tr("Received client context menu action with an invalid client database id: %o."), clientDatabaseId); + return; + } + + const handler = server_connections.findConnection(handlerId); + if(!handler) { return; } + + let clients = handler.channelTree.clients.filter(client => client.properties.client_unique_identifier === clientUniqueId); + if(clientId) { + clients = clients.filter(client => client.clientId() === clientId); + } + if(clientDatabaseId) { + clients = clients.filter(client => client.properties.client_database_id === clientDatabaseId); + } + + clients[0]?.showContextMenu(pageX, pageY); + break; + } + case "contextmenu-channel": { + const { + handlerId, + channelId, + + pageX, + pageY + } = payload; + + if(typeof pageX !== "number" || typeof pageY !== "number") { + logWarn(LogCategory.IPC, tr("Received channel context menu action with an invalid page coordinated: %ox%o."), pageX, pageY); + return; + } + + if(typeof handlerId !== "string") { + logWarn(LogCategory.IPC, tr("Received channel context menu action with an invalid handler id: %o."), handlerId); + return; + } + + if(typeof channelId !== "number") { + logWarn(LogCategory.IPC, tr("Received channel context menu action with an invalid channel id: %o."), channelId); + return; + } + + const handler = server_connections.findConnection(handlerId); + const channel = handler?.channelTree.findChannel(channelId); + channel?.showContextMenu(pageX, pageY); + break; + } + } +} + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "entry tags", + priority: 10, + function: async () => { + const channel = getIpcInstance().createChannel(undefined, kIpcChannel); + channel.messageHandler = (_remoteId, _broadcast, message) => handleIpcMessage(message.type, message.data); + } +}); \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/Controller.ts b/shared/js/ui/react-elements/external-modal/Controller.ts index 215556eb..2291a63b 100644 --- a/shared/js/ui/react-elements/external-modal/Controller.ts +++ b/shared/js/ui/react-elements/external-modal/Controller.ts @@ -29,7 +29,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas this.modalType = modal; this.userData = userData; - this.ipcChannel = ipc.getInstance().createChannel(); + this.ipcChannel = ipc.getIpcInstance().createChannel(); this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this); this.documentUnloadListener = () => this.destroy(); @@ -109,7 +109,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas this.doDestroyWindow(); if(this.ipcChannel) { - ipc.getInstance().deleteChannel(this.ipcChannel); + ipc.getIpcInstance().deleteChannel(this.ipcChannel); } this.destroyIPC(); diff --git a/shared/js/ui/react-elements/external-modal/PopoutController.ts b/shared/js/ui/react-elements/external-modal/PopoutController.ts index ce393a99..12c6da48 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutController.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutController.ts @@ -1,4 +1,4 @@ -import {getInstance as getIPCInstance} from "../../../ipc/BrowserIPC"; +import {getIpcInstance as getIPCInstance} from "../../../ipc/BrowserIPC"; import {Settings, SettingsKey} from "../../../settings"; import { Controller2PopoutMessages, EventControllerBase, diff --git a/shared/js/ui/tree/EntryTags.tsx b/shared/js/ui/tree/EntryTags.tsx index bed7617c..4a39340a 100644 --- a/shared/js/ui/tree/EntryTags.tsx +++ b/shared/js/ui/tree/EntryTags.tsx @@ -1,23 +1,59 @@ import * as React from "react"; +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC"; +import {Settings} from "tc-shared/settings"; +const kIpcChannel = "entry-tags"; const cssStyle = require("./EntryTags.scss"); -export const ClientTag = (props: { clientName: string, clientUniqueId: string, handlerId: string, clientId?: number, clientDatabaseId?: number, className?: string }) => { +let ipcChannel: IPCChannel; - return ( -
{ - event.preventDefault(); +export const ClientTag = (props: { clientName: string, clientUniqueId: string, handlerId: string, clientId?: number, clientDatabaseId?: number, className?: string }) => ( +
{ + event.preventDefault(); - /* TODO: Enable context menus */ - }} - > - {props.clientName} -
- ); -}; + ipcChannel.sendMessage("contextmenu-client", { + clientUniqueId: props.clientUniqueId, + handlerId: props.handlerId, + clientId: props.clientId, + clientDatabaseId: props.clientDatabaseId, -export const ChannelTag = (props: { channelName: string, channelId: number, handlerId: string, className?: string }) => { + pageX: event.pageX, + pageY: event.pageY + }); + }} + > + {props.clientName} +
+); - return
{props.channelName}
; -}; \ No newline at end of file +export const ChannelTag = (props: { channelName: string, channelId: number, handlerId: string, className?: string }) => ( +
{ + event.preventDefault(); + + ipcChannel.sendMessage("contextmenu-channel", { + handlerId: props.handlerId, + channelId: props.channelId, + + pageX: event.pageX, + pageY: event.pageY + }); + }} + > + {props.channelName} +
+); + + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + name: "entry tags", + priority: 10, + function: async () => { + const ipc = getIpcInstance(); + ipcChannel = ipc.createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, ipc.getLocalAddress()), kIpcChannel); + } +}); \ No newline at end of file diff --git a/web/app/ExternalModalFactory.ts b/web/app/ExternalModalFactory.ts index 3ca3773c..e7e70593 100644 --- a/web/app/ExternalModalFactory.ts +++ b/web/app/ExternalModalFactory.ts @@ -87,7 +87,7 @@ export class ExternalModalController extends AbstractExternalModalController { "chunk": "modal-external", "modal-target": this.modalType, "ipc-channel": this.ipcChannel.channelId, - "ipc-address": ipc.getInstance().getLocalAddress(), + "ipc-address": ipc.getIpcInstance().getLocalAddress(), "disableGlobalContextMenu": __build.mode === "debug" ? 1 : 0, "loader-abort": __build.mode === "debug" ? 1 : 0, }; diff --git a/web/app/ui/context-menu/Ipc.ts b/web/app/ui/context-menu/Ipc.ts index dcad3e97..f35a0b0a 100644 --- a/web/app/ui/context-menu/Ipc.ts +++ b/web/app/ui/context-menu/Ipc.ts @@ -26,7 +26,7 @@ class IPCContextMenu implements ContextMenuFactory { private closeCallback: () => void; constructor() { - this.ipcChannel = ipc.getInstance().createChannel(undefined, kIPCContextMenuChannel); + this.ipcChannel = ipc.getIpcInstance().createChannel(undefined, kIPCContextMenuChannel); this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); /* if we're just created we're the focused window ;) */