Some more channel tree features
This commit is contained in:
parent
df8136acb1
commit
b46a7f59f5
9 changed files with 125 additions and 6 deletions
|
@ -8,6 +8,7 @@
|
||||||
- Fixed the invalid initialisation of codec workers
|
- Fixed the invalid initialisation of codec workers
|
||||||
- Improved context menu subcontainer selection
|
- Improved context menu subcontainer selection
|
||||||
- Fixed client channel permission tab within the permission editor (previously you've been kick from the server)
|
- Fixed client channel permission tab within the permission editor (previously you've been kick from the server)
|
||||||
|
- Added the ability to collapse/expend the channel tree
|
||||||
|
|
||||||
* **11.04.20**
|
* **11.04.20**
|
||||||
- Only show the host message when its not empty
|
- Only show the host message when its not empty
|
||||||
|
|
|
@ -19,6 +19,7 @@ import * as React from "react";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||||
import { ChannelEntryView as ChannelEntryView } from "./tree/Channel";
|
import { ChannelEntryView as ChannelEntryView } from "./tree/Channel";
|
||||||
|
import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu";
|
||||||
|
|
||||||
export enum ChannelType {
|
export enum ChannelType {
|
||||||
PERMANENT,
|
PERMANENT,
|
||||||
|
@ -88,6 +89,9 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
|
||||||
notify_subscribe_state_changed: {
|
notify_subscribe_state_changed: {
|
||||||
channel_subscribed: boolean
|
channel_subscribed: boolean
|
||||||
},
|
},
|
||||||
|
notify_collapsed_state_changed: {
|
||||||
|
collapsed: boolean
|
||||||
|
},
|
||||||
|
|
||||||
notify_children_changed: {},
|
notify_children_changed: {},
|
||||||
notify_clients_changed: {}, /* will also be fired when clients haven been reordered */
|
notify_clients_changed: {}, /* will also be fired when clients haven been reordered */
|
||||||
|
@ -157,7 +161,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
child_channel_head?: ChannelEntry;
|
child_channel_head?: ChannelEntry;
|
||||||
|
|
||||||
readonly events: Registry<ChannelEvents>;
|
readonly events: Registry<ChannelEvents>;
|
||||||
readonly view: React.Ref<ChannelEntryView>;
|
readonly view: React.RefObject<ChannelEntryView>;
|
||||||
|
|
||||||
parsed_channel_name: ParsedChannelName;
|
parsed_channel_name: ParsedChannelName;
|
||||||
|
|
||||||
|
@ -172,6 +176,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
private _cached_channel_description_promise_resolve: any = undefined;
|
private _cached_channel_description_promise_resolve: any = undefined;
|
||||||
private _cached_channel_description_promise_reject: any = undefined;
|
private _cached_channel_description_promise_reject: any = undefined;
|
||||||
|
|
||||||
|
private _flag_collapsed: boolean; //TODO: Load from config!
|
||||||
private _flag_subscribed: boolean;
|
private _flag_subscribed: boolean;
|
||||||
private _subscribe_mode: ChannelSubscribeMode;
|
private _subscribe_mode: ChannelSubscribeMode;
|
||||||
|
|
||||||
|
@ -378,6 +383,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
|
|
||||||
let trigger_close = true;
|
let trigger_close = true;
|
||||||
|
|
||||||
|
const collapse_expendable = !!this.child_channel_head || this.client_list.length > 0;
|
||||||
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
|
const bold = text => contextmenu.get_provider().html_format_enabled() ? "<b>" + text + "</b>" : text;
|
||||||
contextmenu.spawn_context_menu(x, y, {
|
contextmenu.spawn_context_menu(x, y, {
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
@ -497,6 +503,25 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: MenuEntryType.HR,
|
||||||
|
name: "",
|
||||||
|
visible: collapse_expendable
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-channel_collapse_all",
|
||||||
|
name: tr("Collapse sub channels"),
|
||||||
|
visible: collapse_expendable,
|
||||||
|
callback: () => this.channelTree.collapse_channels(this)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-channel_expand_all",
|
||||||
|
name: tr("Expend sub channels"),
|
||||||
|
visible: collapse_expendable,
|
||||||
|
callback: () => this.channelTree.expand_channels(this)
|
||||||
|
},
|
||||||
contextmenu.Entry.HR(),
|
contextmenu.Entry.HR(),
|
||||||
{
|
{
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
@ -665,6 +690,18 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get collapsed() : boolean {
|
||||||
|
return this._flag_collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
set collapsed(flag: boolean) {
|
||||||
|
if(this._flag_collapsed === flag)
|
||||||
|
return;
|
||||||
|
this._flag_collapsed = flag;
|
||||||
|
this.events.fire("notify_collapsed_state_changed", { collapsed: flag });
|
||||||
|
this.view.current?.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
get flag_subscribed() : boolean {
|
get flag_subscribed() : boolean {
|
||||||
return this._flag_subscribed;
|
return this._flag_subscribed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,23 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
name: tr("View avatars"),
|
name: tr("View avatars"),
|
||||||
visible: false, //TODO: Enable again as soon the new design is finished
|
visible: false, //TODO: Enable again as soon the new design is finished
|
||||||
callback: () => spawnAvatarList(this.channelTree.client)
|
callback: () => spawnAvatarList(this.channelTree.client)
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.HR,
|
||||||
|
name: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-channel_collapse_all",
|
||||||
|
name: tr("Collapse all channels"),
|
||||||
|
callback: () => this.channelTree.collapse_channels()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-channel_expand_all",
|
||||||
|
name: tr("Expend all channels"),
|
||||||
|
callback: () => this.channelTree.expand_channels()
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
.containerArrow {
|
||||||
|
width: 16px;
|
||||||
|
margin-left: -16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
align-self: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .arrow {
|
||||||
|
border-color: hsla(220, 5%, 30%, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.channelType {
|
.channelType {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
|
@ -220,6 +220,14 @@ interface ChannelEntryViewProperties {
|
||||||
offset: number;
|
offset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ChannelCollapsedIndicator = (props: { collapsed: boolean, onToggle: () => void }) => {
|
||||||
|
return <div className={channelStyle.containerArrow + (!props.collapsed ? " " + channelStyle.down : "")}><div className={"arrow " + (props.collapsed ? "right" : "down")} onClick={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
props.onToggle();
|
||||||
|
}} /></div>
|
||||||
|
};
|
||||||
|
|
||||||
@ReactEventHandler<ChannelEntryView>(e => e.props.channel.events)
|
@ReactEventHandler<ChannelEntryView>(e => e.props.channel.events)
|
||||||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||||
export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}> {
|
export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}> {
|
||||||
|
@ -233,19 +241,25 @@ export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}>
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const collapsed_indicator = this.props.channel.child_channel_head || this.props.channel.clients(false).length > 0;
|
||||||
return <div className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
|
return <div className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
|
||||||
style={{ paddingLeft: this.props.depth * 16, top: this.props.offset }}
|
style={{ paddingLeft: this.props.depth * 16 + 2, top: this.props.offset }}
|
||||||
onMouseDown={e => this.onMouseDown(e as any)}
|
onMouseDown={e => this.onMouseDown(e as any)}
|
||||||
onDoubleClick={() => this.onDoubleClick()}
|
onDoubleClick={() => this.onDoubleClick()}
|
||||||
onContextMenu={e => this.onContextMenu(e as any)}
|
onContextMenu={e => this.onContextMenu(e as any)}
|
||||||
>
|
>
|
||||||
<UnreadMarker entry={this.props.channel} />
|
<UnreadMarker entry={this.props.channel} />
|
||||||
|
{collapsed_indicator && <ChannelCollapsedIndicator key={"collapsed-indicator"} onToggle={() => this.onCollapsedToggle()} collapsed={this.props.channel.collapsed} />}
|
||||||
<ChannelEntryIcon channel={this.props.channel} />
|
<ChannelEntryIcon channel={this.props.channel} />
|
||||||
<ChannelEntryName channel={this.props.channel} />
|
<ChannelEntryName channel={this.props.channel} />
|
||||||
<ChannelEntryIcons channel={this.props.channel} />
|
<ChannelEntryIcons channel={this.props.channel} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onCollapsedToggle() {
|
||||||
|
this.props.channel.collapsed = !this.props.channel.collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
private onMouseDown(event: MouseEvent) {
|
private onMouseDown(event: MouseEvent) {
|
||||||
if(event.button !== 0) return; /* only left mouse clicks */
|
if(event.button !== 0) return; /* only left mouse clicks */
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ export class ClientEntry extends TreeEntry<ClientEntryProperties, ClientEntrySta
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={this.classList(clientStyle.clientEntry, viewStyle.treeEntry, this.props.client.isSelected() && viewStyle.selected)}
|
<div className={this.classList(clientStyle.clientEntry, viewStyle.treeEntry, this.props.client.isSelected() && viewStyle.selected)}
|
||||||
style={{ paddingLeft: (this.props.depth * 16) + "px", top: this.props.offset }}
|
style={{ paddingLeft: (this.props.depth * 16 + 2) + "px", top: this.props.offset }}
|
||||||
onDoubleClick={() => this.onDoubleClick()}
|
onDoubleClick={() => this.onDoubleClick()}
|
||||||
onMouseDown={e => this.onMouseDown(e as any)}
|
onMouseDown={e => this.onMouseDown(e as any)}
|
||||||
onContextMenu={e => this.onContextMenu(e as any)}
|
onContextMenu={e => this.onContextMenu(e as any)}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 2px;
|
padding-left: 2px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
.server_type {
|
.server_type {
|
||||||
|
@ -14,7 +14,6 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
margin-left: 2px;
|
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
private flat_tree: FlatTreeEntry[] = [];
|
private flat_tree: FlatTreeEntry[] = [];
|
||||||
private listener_client_change;
|
private listener_client_change;
|
||||||
private listener_channel_change;
|
private listener_channel_change;
|
||||||
|
private listener_state_collapsed;
|
||||||
private update_timeout;
|
private update_timeout;
|
||||||
|
|
||||||
private in_view_callbacks: {
|
private in_view_callbacks: {
|
||||||
|
@ -93,6 +94,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
(window as any).do_tree_update = () => this.handleTreeUpdate();
|
(window as any).do_tree_update = () => this.handleTreeUpdate();
|
||||||
this.listener_client_change = () => this.handleTreeUpdate();
|
this.listener_client_change = () => this.handleTreeUpdate();
|
||||||
this.listener_channel_change = () => this.handleTreeUpdate();
|
this.listener_channel_change = () => this.handleTreeUpdate();
|
||||||
|
this.listener_state_collapsed = () => this.handleTreeUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTreeUpdate() {
|
private handleTreeUpdate() {
|
||||||
|
@ -147,11 +149,14 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
private build_sub_tree(entry: ChannelEntry, depth: number) {
|
private build_sub_tree(entry: ChannelEntry, depth: number) {
|
||||||
entry.events.on("notify_clients_changed", this.listener_client_change);
|
entry.events.on("notify_clients_changed", this.listener_client_change);
|
||||||
entry.events.on("notify_children_changed", this.listener_channel_change);
|
entry.events.on("notify_children_changed", this.listener_channel_change);
|
||||||
|
entry.events.on("notify_collapsed_state_changed", this.listener_state_collapsed);
|
||||||
|
|
||||||
this.flat_tree.push({
|
this.flat_tree.push({
|
||||||
entry: entry,
|
entry: entry,
|
||||||
rendered: <ChannelEntryView key={"channel-" + entry.channelId} channel={entry} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth} ref={entry.view} />
|
rendered: <ChannelEntryView key={"channel-" + entry.channelId} channel={entry} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth} ref={entry.view} />
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(entry.collapsed) return;
|
||||||
this.flat_tree.push(...entry.clients(false).map(e => {
|
this.flat_tree.push(...entry.clients(false).map(e => {
|
||||||
return {
|
return {
|
||||||
entry: e,
|
entry: e,
|
||||||
|
@ -171,6 +176,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
if(entry instanceof ChannelEntry) {
|
if(entry instanceof ChannelEntry) {
|
||||||
entry.events.off("notify_clients_changed", this.listener_client_change);
|
entry.events.off("notify_clients_changed", this.listener_client_change);
|
||||||
entry.events.off("notify_children_changed", this.listener_channel_change);
|
entry.events.off("notify_children_changed", this.listener_channel_change);
|
||||||
|
entry.events.off("notify_collapsed_state_changed", this.listener_state_collapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,6 +291,18 @@ export class ChannelTree {
|
||||||
invalidPermission: !channelCreate,
|
invalidPermission: !channelCreate,
|
||||||
callback: () => this.spawnCreateChannel()
|
callback: () => this.spawnCreateChannel()
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-channel_collapse_all",
|
||||||
|
name: tr("Collapse all channels"),
|
||||||
|
callback: () => this.collapse_channels()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
|
icon_class: "client-channel_expand_all",
|
||||||
|
name: tr("Expend all channels"),
|
||||||
|
callback: () => this.expand_channels()
|
||||||
|
},
|
||||||
contextmenu.Entry.CLOSE(on_close)
|
contextmenu.Entry.CLOSE(on_close)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1049,4 +1061,24 @@ export class ChannelTree {
|
||||||
console.warn(tr("Failed to subscribe to all channels! (%o)"), error);
|
console.warn(tr("Failed to subscribe to all channels! (%o)"), error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expand_channels(root?: ChannelEntry) {
|
||||||
|
if(typeof root === "undefined")
|
||||||
|
this.rootChannel().forEach(e => this.expand_channels(e));
|
||||||
|
else {
|
||||||
|
root.collapsed = false;
|
||||||
|
for(const child of root.children(false))
|
||||||
|
this.expand_channels(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse_channels(root?: ChannelEntry) {
|
||||||
|
if(typeof root === "undefined")
|
||||||
|
this.rootChannel().forEach(e => this.collapse_channels(e));
|
||||||
|
else {
|
||||||
|
root.collapsed = true;
|
||||||
|
for(const child of root.children(false))
|
||||||
|
this.collapse_channels(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue