Some more channel tree features

canary
WolverinDEV 2020-04-18 21:26:44 +02:00
parent df8136acb1
commit b46a7f59f5
9 changed files with 125 additions and 6 deletions

View File

@ -8,6 +8,7 @@
- Fixed the invalid initialisation of codec workers
- Improved context menu subcontainer selection
- 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**
- Only show the host message when its not empty

View File

@ -19,6 +19,7 @@ import * as React from "react";
import {Registry} from "tc-shared/events";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
import { ChannelEntryView as ChannelEntryView } from "./tree/Channel";
import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu";
export enum ChannelType {
PERMANENT,
@ -88,6 +89,9 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
notify_subscribe_state_changed: {
channel_subscribed: boolean
},
notify_collapsed_state_changed: {
collapsed: boolean
},
notify_children_changed: {},
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;
readonly events: Registry<ChannelEvents>;
readonly view: React.Ref<ChannelEntryView>;
readonly view: React.RefObject<ChannelEntryView>;
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_reject: any = undefined;
private _flag_collapsed: boolean; //TODO: Load from config!
private _flag_subscribed: boolean;
private _subscribe_mode: ChannelSubscribeMode;
@ -378,6 +383,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
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;
contextmenu.spawn_context_menu(x, y, {
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(),
{
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 {
return this._flag_subscribed;
}

View File

@ -245,7 +245,23 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
name: tr("View avatars"),
visible: false, //TODO: Enable again as soon the new design is finished
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()
},
];
}

View File

@ -11,6 +11,20 @@
align-items: center;
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 {
flex-grow: 0;
flex-shrink: 0;

View File

@ -220,6 +220,14 @@ interface ChannelEntryViewProperties {
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)
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}> {
@ -233,19 +241,25 @@ export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}>
}
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)}
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)}
onDoubleClick={() => this.onDoubleClick()}
onContextMenu={e => this.onContextMenu(e as any)}
>
<UnreadMarker entry={this.props.channel} />
{collapsed_indicator && <ChannelCollapsedIndicator key={"collapsed-indicator"} onToggle={() => this.onCollapsedToggle()} collapsed={this.props.channel.collapsed} />}
<ChannelEntryIcon channel={this.props.channel} />
<ChannelEntryName channel={this.props.channel} />
<ChannelEntryIcons channel={this.props.channel} />
</div>;
}
private onCollapsedToggle() {
this.props.channel.collapsed = !this.props.channel.collapsed;
}
private onMouseDown(event: MouseEvent) {
if(event.button !== 0) return; /* only left mouse clicks */

View File

@ -362,7 +362,7 @@ export class ClientEntry extends TreeEntry<ClientEntryProperties, ClientEntrySta
render() {
return (
<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()}
onMouseDown={e => this.onMouseDown(e as any)}
onContextMenu={e => this.onContextMenu(e as any)}

View File

@ -6,7 +6,7 @@
position: relative;
cursor: pointer;
margin-left: 2px;
padding-left: 2px;
margin-right: 5px;
.server_type {
@ -14,7 +14,6 @@
flex-shrink: 0;
margin-right: 2px;
margin-left: 2px;
z-index: 1;
}

View File

@ -49,6 +49,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
private flat_tree: FlatTreeEntry[] = [];
private listener_client_change;
private listener_channel_change;
private listener_state_collapsed;
private update_timeout;
private in_view_callbacks: {
@ -93,6 +94,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
(window as any).do_tree_update = () => this.handleTreeUpdate();
this.listener_client_change = () => this.handleTreeUpdate();
this.listener_channel_change = () => this.handleTreeUpdate();
this.listener_state_collapsed = () => this.handleTreeUpdate();
}
private handleTreeUpdate() {
@ -147,11 +149,14 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
private build_sub_tree(entry: ChannelEntry, depth: number) {
entry.events.on("notify_clients_changed", this.listener_client_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({
entry: entry,
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 => {
return {
entry: e,
@ -171,6 +176,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
if(entry instanceof ChannelEntry) {
entry.events.off("notify_clients_changed", this.listener_client_change);
entry.events.off("notify_children_changed", this.listener_channel_change);
entry.events.off("notify_collapsed_state_changed", this.listener_state_collapsed);
}
}
}

View File

@ -291,6 +291,18 @@ export class ChannelTree {
invalidPermission: !channelCreate,
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)
);
}
@ -1049,4 +1061,24 @@ export class ChannelTree {
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);
}
}
}