split off context-menu into sub-functional component
This commit is contained in:
parent
6f26181b43
commit
f7433c23be
@ -20,27 +20,4 @@ export default class Actions {
|
|||||||
ui.setActiveConnection(guild, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null });
|
ui.setActiveConnection(guild, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchAndUpdateChannels(q: Q, ui: UI, guild: CombinedGuild) {
|
|
||||||
await Util.withPotentialErrorWarnOnCancel(q, {
|
|
||||||
taskFunc: async () => {
|
|
||||||
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
|
|
||||||
const channels = await guild.fetchChannels();
|
|
||||||
await ui.setChannels(guild, channels);
|
|
||||||
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
|
|
||||||
if (ui.activeChannel === null) {
|
|
||||||
// click on the first channel in the list if no channel is active yet
|
|
||||||
const element = q.$_('#channel-list .channel');
|
|
||||||
if (element) {
|
|
||||||
element.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errorIndicatorAddFunc: async (errorIndicatorElement) => {
|
|
||||||
await ui.setChannelsErrorIndicator(guild, errorIndicatorElement);
|
|
||||||
},
|
|
||||||
errorContainer: q.$('#channel-list'),
|
|
||||||
errorMessage: 'Error fetching channels'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactHelper from './require/react-helper';
|
|
||||||
|
|
||||||
import ElementsUtil from './require/elements-util';
|
|
||||||
import BaseElements from './require/base-elements';
|
|
||||||
import { Channel } from '../data-types';
|
|
||||||
import UI from '../ui';
|
|
||||||
import Q from '../q-module';
|
|
||||||
import CombinedGuild from '../guild-combined';
|
|
||||||
import ChannelOverlay from './overlays/overlay-channel';
|
|
||||||
|
|
||||||
export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
|
|
||||||
const element = ReactHelper.createElementFromJSX(
|
|
||||||
<div className="channel text" data-id={channel.id} data-guild-id={guild.id}>
|
|
||||||
<div className="icon">{BaseElements.TEXT_CHANNEL_ICON}</div>
|
|
||||||
<div className="name">{channel.name}</div>
|
|
||||||
<div className="modify">{BaseElements.COG}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener('click', async () => {
|
|
||||||
if (element.classList.contains('active')) return;
|
|
||||||
await ui.setActiveChannel(guild, channel);
|
|
||||||
q.$('#text-input').focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
const modifyContextElement = q.create({ class: 'context', content: {
|
|
||||||
class: 'above', content: [
|
|
||||||
{ class: 'content text', content: 'Modify Channel' },
|
|
||||||
{ class: 'tab', content: BaseElements.Q_TAB_BELOW }
|
|
||||||
]
|
|
||||||
}}) as HTMLElement;
|
|
||||||
|
|
||||||
q.$$$(element, '.modify').addEventListener('click', async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (modifyContextElement.parentElement) {
|
|
||||||
modifyContextElement.parentElement.removeChild(modifyContextElement);
|
|
||||||
}
|
|
||||||
ElementsUtil.presentReactOverlay(document, <ChannelOverlay guild={guild} channel={channel} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
q.$$$(element, '.modify').addEventListener('mouseenter', () => {
|
|
||||||
document.body.appendChild(modifyContextElement);
|
|
||||||
ElementsUtil.alignContextElement(modifyContextElement, q.$$$(element, '.modify'), { bottom: 'top', centerX: 'centerX' });
|
|
||||||
});
|
|
||||||
|
|
||||||
q.$$$(element, '.modify').addEventListener('mouseleave', () => {
|
|
||||||
if (modifyContextElement.parentElement) {
|
|
||||||
modifyContextElement.parentElement.removeChild(modifyContextElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import React, { FC, ReactNode, RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { ShouldNeverHappenError } from '../../../data-types';
|
|
||||||
import ElementsUtil, { IAlignment } from '../../require/elements-util';
|
|
||||||
import ReactHelper from '../../require/react-helper';
|
|
||||||
|
|
||||||
export interface ContextMenuProps {
|
|
||||||
relativeToRef?: RefObject<HTMLElement | null>;
|
|
||||||
relativeToPos?: { x: number, y: number };
|
|
||||||
alignment: IAlignment;
|
|
||||||
children: ReactNode;
|
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextMenu: FC<ContextMenuProps> = (props: ContextMenuProps) => {
|
|
||||||
const { relativeToRef, relativeToPos, alignment, children, close } = props;
|
|
||||||
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [ aligned, setAligned ] = useState<boolean>(false);
|
|
||||||
|
|
||||||
ReactHelper.useCloseWhenClickedOutsideEffect(rootRef, close);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!rootRef.current) return;
|
|
||||||
const relativeTo = (relativeToRef && relativeToRef.current) ?? relativeToPos ?? null;
|
|
||||||
if (!relativeTo) throw new ShouldNeverHappenError('invalid context menu props');
|
|
||||||
ElementsUtil.alignContextElement(rootRef.current, relativeTo, alignment);
|
|
||||||
setAligned(true);
|
|
||||||
}, [ rootRef, relativeToRef, relativeToPos ]);
|
|
||||||
|
|
||||||
const contextClass = useMemo(() => {
|
|
||||||
return 'context react' + (aligned ? ' aligned' : '');
|
|
||||||
}, [ aligned ]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={rootRef} className={contextClass}>
|
|
||||||
<div className="menu">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContextMenu;
|
|
@ -0,0 +1,31 @@
|
|||||||
|
import React, { FC, ReactNode, RefObject, useRef } from 'react'
|
||||||
|
import { IAlignment } from '../../require/elements-util';
|
||||||
|
import ReactHelper from '../../require/react-helper';
|
||||||
|
import Context from './context';
|
||||||
|
|
||||||
|
export interface ContextMenuProps {
|
||||||
|
relativeToRef?: RefObject<HTMLElement>;
|
||||||
|
relativeToPos?: { x: number, y: number };
|
||||||
|
alignment: IAlignment;
|
||||||
|
children: ReactNode;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically closes when clicked outside of and includes a <div class="menu"> subelement
|
||||||
|
const ContextMenu: FC<ContextMenuProps> = (props: ContextMenuProps) => {
|
||||||
|
const { relativeToRef, relativeToPos, alignment, children, close } = props;
|
||||||
|
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
ReactHelper.useCloseWhenClickedOutsideEffect(rootRef, close);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context rootRef={rootRef} relativeToRef={relativeToRef} relativeToPos={relativeToPos} alignment={alignment}>
|
||||||
|
<div className="menu">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Context>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextMenu;
|
28
src/client/webapp/elements/contexts/components/context.tsx
Normal file
28
src/client/webapp/elements/contexts/components/context.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, { FC, ReactNode, RefObject } from 'react';
|
||||||
|
import { IAlignment } from '../../require/elements-util';
|
||||||
|
import ReactHelper from '../../require/react-helper';
|
||||||
|
|
||||||
|
export interface ContextProps {
|
||||||
|
rootRef: RefObject<HTMLDivElement>;
|
||||||
|
relativeToRef?: RefObject<HTMLElement>;
|
||||||
|
relativeToPos?: { x: number, y: number };
|
||||||
|
alignment: IAlignment;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// You should create a component like context-menu.tsx instead of using this class directly.
|
||||||
|
const Context: FC<ContextProps> = (props: ContextProps) => {
|
||||||
|
const { rootRef, relativeToRef, relativeToPos, alignment, children } = props;
|
||||||
|
|
||||||
|
const [ className ] = ReactHelper.useAlignment(
|
||||||
|
rootRef, relativeToRef ?? null, relativeToPos ?? null, alignment, 'context react'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className={className}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Context;
|
@ -8,7 +8,7 @@ import ContextMenu from './components/context-menu';
|
|||||||
export interface ConnectionInfoContextMenuProps {
|
export interface ConnectionInfoContextMenuProps {
|
||||||
guild: CombinedGuild;
|
guild: CombinedGuild;
|
||||||
selfMember: Member;
|
selfMember: Member;
|
||||||
relativeToRef: RefObject<HTMLElement | null>;
|
relativeToRef: RefObject<HTMLElement>;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -95,11 +95,6 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, g
|
|||||||
ui.updateGuildName(guild, 'ERROR');
|
ui.updateGuildName(guild, 'ERROR');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Guild Channel List
|
|
||||||
(async () => {
|
|
||||||
await Actions.fetchAndUpdateChannels(q, ui, guild);
|
|
||||||
})();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
element.addEventListener('contextmenu', (e) => {
|
element.addEventListener('contextmenu', (e) => {
|
||||||
|
@ -2,7 +2,7 @@ import moment from 'moment';
|
|||||||
import React, { FC, MouseEvent, useCallback, useMemo, useState } from 'react';
|
import React, { FC, MouseEvent, useCallback, useMemo, useState } from 'react';
|
||||||
import { Member, Message } from '../../../data-types';
|
import { Member, Message } from '../../../data-types';
|
||||||
import CombinedGuild from '../../../guild-combined';
|
import CombinedGuild from '../../../guild-combined';
|
||||||
import ImageContextMenu from '../../context-menus/context-menu-image';
|
import ImageContextMenu from '../../contexts/context-menu-image';
|
||||||
import ImageOverlay from '../../overlays/overlay-image';
|
import ImageOverlay from '../../overlays/overlay-image';
|
||||||
import ElementsUtil from '../../require/elements-util';
|
import ElementsUtil from '../../require/elements-util';
|
||||||
import GuildSubscriptions from '../../require/guild-subscriptions';
|
import GuildSubscriptions from '../../require/guild-subscriptions';
|
||||||
|
@ -9,7 +9,7 @@ import ElementsUtil from '../require/elements-util';
|
|||||||
import DownloadButton from '../components/button-download';
|
import DownloadButton from '../components/button-download';
|
||||||
import ReactHelper from '../require/react-helper';
|
import ReactHelper from '../require/react-helper';
|
||||||
import GuildSubscriptions from '../require/guild-subscriptions';
|
import GuildSubscriptions from '../require/guild-subscriptions';
|
||||||
import ImageContextMenu from '../context-menus/context-menu-image';
|
import ImageContextMenu from '../contexts/context-menu-image';
|
||||||
import Overlay from '../components/overlay';
|
import Overlay from '../components/overlay';
|
||||||
|
|
||||||
export interface ImageOverlayProps {
|
export interface ImageOverlayProps {
|
||||||
|
@ -12,6 +12,7 @@ import Globals from '../../globals';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
|
import ElementsUtil, { IAlignment } from './elements-util';
|
||||||
|
|
||||||
// Helper function so we can use JSX before fully committing to React
|
// Helper function so we can use JSX before fully committing to React
|
||||||
|
|
||||||
@ -396,6 +397,32 @@ export default class ReactHelper {
|
|||||||
}, [ handleMouseUp ]);
|
}, [ handleMouseUp ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static useAlignment(
|
||||||
|
rootRef: RefObject<HTMLElement>,
|
||||||
|
relativeToRef: RefObject<HTMLElement | null> | null,
|
||||||
|
relativeToPos: { x: number, y: number } | null,
|
||||||
|
alignment: IAlignment,
|
||||||
|
baseClassName: string
|
||||||
|
): [
|
||||||
|
className: string
|
||||||
|
] {
|
||||||
|
const [ aligned, setAligned ] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rootRef.current) return;
|
||||||
|
const relativeTo = (relativeToRef && relativeToRef.current) ?? relativeToPos ?? null;
|
||||||
|
if (!relativeTo) throw new ShouldNeverHappenError('invalid alignment props');
|
||||||
|
ElementsUtil.alignContextElement(rootRef.current, relativeTo, alignment);
|
||||||
|
setAligned(true);
|
||||||
|
}, [ rootRef, relativeToRef, relativeToPos ]);
|
||||||
|
|
||||||
|
const className = useMemo(() => {
|
||||||
|
return baseClassName + (aligned ? ' aligned' : '');
|
||||||
|
}, [ baseClassName, aligned ]);
|
||||||
|
|
||||||
|
return [ className ];
|
||||||
|
}
|
||||||
|
|
||||||
static useContextMenu(
|
static useContextMenu(
|
||||||
createContextMenu: (close: () => void) => ReactNode,
|
createContextMenu: (close: () => void) => ReactNode,
|
||||||
createContextMenuDeps: DependencyList
|
createContextMenuDeps: DependencyList
|
||||||
|
@ -8,7 +8,7 @@ import { Member } from '../../data-types';
|
|||||||
import CombinedGuild from '../../guild-combined';
|
import CombinedGuild from '../../guild-combined';
|
||||||
import MemberElement, { DummyMember } from '../lists/components/member-element';
|
import MemberElement, { DummyMember } from '../lists/components/member-element';
|
||||||
import GuildSubscriptions from '../require/guild-subscriptions';
|
import GuildSubscriptions from '../require/guild-subscriptions';
|
||||||
import ConnectionInfoContextMenu from '../context-menus/context-menu-connection-info';
|
import ConnectionInfoContextMenu from '../contexts/context-menu-connection-info';
|
||||||
import ReactHelper from '../require/react-helper';
|
import ReactHelper from '../require/react-helper';
|
||||||
|
|
||||||
export interface ConnectionInfoProps {
|
export interface ConnectionInfoProps {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC, useRef } from 'react';
|
import React, { FC, useRef } from 'react';
|
||||||
import CombinedGuild from '../../guild-combined';
|
import CombinedGuild from '../../guild-combined';
|
||||||
import GuildTitleContextMenu from '../context-menus/context-menu-guild-title';
|
import GuildTitleContextMenu from '../contexts/context-menu-guild-title';
|
||||||
import GuildSubscriptions from '../require/guild-subscriptions';
|
import GuildSubscriptions from '../require/guild-subscriptions';
|
||||||
import ReactHelper from '../require/react-helper';
|
import ReactHelper from '../require/react-helper';
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import Globals from './globals';
|
|||||||
|
|
||||||
import UI from './ui';
|
import UI from './ui';
|
||||||
import Actions from './actions';
|
import Actions from './actions';
|
||||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, Member, Resource, Token } from './data-types';
|
import { Changes, ConnectionInfo, GuildMetadata, Member, Resource, Token } from './data-types';
|
||||||
import Q from './q-module';
|
import Q from './q-module';
|
||||||
import bindWindowButtonEvents from './elements/events-window-buttons';
|
import bindWindowButtonEvents from './elements/events-window-buttons';
|
||||||
import bindTextInputEvents from './elements/events-text-input';
|
import bindTextInputEvents from './elements/events-text-input';
|
||||||
@ -90,10 +90,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
(async () => { // update connection info
|
(async () => { // update connection info
|
||||||
await Actions.fetchAndUpdateConnection(ui, guild);
|
await Actions.fetchAndUpdateConnection(ui, guild);
|
||||||
})();
|
})();
|
||||||
(async () => { // refresh channels list
|
// TODO: React set hasMessagesAbove and hasMessagesBelow when re-verified?
|
||||||
await Actions.fetchAndUpdateChannels(q, ui, guild);
|
|
||||||
})();
|
|
||||||
// TODO: React set hasMessagesAbove and hasMessagesBelow when re-verified
|
|
||||||
});
|
});
|
||||||
|
|
||||||
guildsManager.on('disconnect', (guild: CombinedGuild) => {
|
guildsManager.on('disconnect', (guild: CombinedGuild) => {
|
||||||
@ -132,21 +129,6 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
guildsManager.on('remove-channels', async (guild: CombinedGuild, channels: Channel[]) => {
|
|
||||||
LOG.debug(channels.length + ' removed channels');
|
|
||||||
await ui.deleteChannels(guild, channels);
|
|
||||||
});
|
|
||||||
|
|
||||||
guildsManager.on('update-channels', async (guild: CombinedGuild, updatedChannels: Channel[]) => {
|
|
||||||
LOG.debug(updatedChannels.length + ' updated channels');
|
|
||||||
await ui.updateChannels(guild, updatedChannels);
|
|
||||||
});
|
|
||||||
|
|
||||||
guildsManager.on('new-channels', async (guild: CombinedGuild, channels: Channel[]) => {
|
|
||||||
LOG.debug(channels.length + ' added channels');
|
|
||||||
await ui.addChannels(guild, channels);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: React jump messages to bottom when the current user sent a message
|
// TODO: React jump messages to bottom when the current user sent a message
|
||||||
|
|
||||||
// Conflict Events
|
// Conflict Events
|
||||||
@ -160,13 +142,6 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
guildsManager.on('conflict-channels', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Channel>) => {
|
|
||||||
LOG.debug('channels conflict', { changes });
|
|
||||||
if (changes.deleted.length > 0) await ui.deleteChannels(guild, changes.deleted);
|
|
||||||
if (changes.added.length > 0) await ui.addChannels(guild, changes.added);
|
|
||||||
if (changes.updated.length > 0) await ui.updateChannels(guild, changes.updated.map(pair => pair.newDataPoint));
|
|
||||||
});
|
|
||||||
|
|
||||||
guildsManager.on('conflict-members', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
|
guildsManager.on('conflict-members', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
|
||||||
//LOG.debug('members conflict', { changes });
|
//LOG.debug('members conflict', { changes });
|
||||||
if (changes.updated.length > 0) {
|
if (changes.updated.length > 0) {
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
@import "theme.scss";
|
@import "theme.scss";
|
||||||
|
|
||||||
#channel-list {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-list {
|
.channel-list {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
@ -11,7 +11,6 @@ import CombinedGuild from './guild-combined';
|
|||||||
import { Message, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
|
import { Message, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
|
||||||
import Q from './q-module';
|
import Q from './q-module';
|
||||||
import createGuildListGuild from './elements/guild-list-guild';
|
import createGuildListGuild from './elements/guild-list-guild';
|
||||||
import createChannel from './elements/channel';
|
|
||||||
import GuildsManager from './guilds-manager';
|
import GuildsManager from './guilds-manager';
|
||||||
import { mountGuildChannelComponents, mountGuildComponents } from './elements/mounts';
|
import { mountGuildChannelComponents, mountGuildComponents } from './elements/mounts';
|
||||||
|
|
||||||
@ -114,22 +113,11 @@ export default class UI {
|
|||||||
|
|
||||||
public async setActiveChannel(guild: CombinedGuild, channel: Channel): Promise<void> {
|
public async setActiveChannel(guild: CombinedGuild, channel: Channel): Promise<void> {
|
||||||
await this.lockChannels(guild, () => {
|
await this.lockChannels(guild, () => {
|
||||||
// Channel List Highlight
|
|
||||||
if (this.activeChannel !== null) {
|
|
||||||
const prev = this.q.$_('#channel-list .channel[data-id="' + this.activeChannel.id + '"]');
|
|
||||||
if (prev) {
|
|
||||||
prev.classList.remove('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const next = this.q.$('#channel-list .channel[data-id="' + channel.id + '"]');
|
|
||||||
next.classList.add('active');
|
|
||||||
|
|
||||||
// Channel Name + Flavor Text Header + Text Input Placeholder
|
// Channel Name + Flavor Text Header + Text Input Placeholder
|
||||||
this.q.$('#text-input').setAttribute('data-placeholder', 'Message #' + channel.name);
|
this.q.$('#text-input').setAttribute('data-placeholder', 'Message #' + channel.name);
|
||||||
|
|
||||||
mountGuildChannelComponents(this.q, guild, channel);
|
|
||||||
|
|
||||||
this.activeChannel = channel;
|
this.activeChannel = channel;
|
||||||
|
mountGuildChannelComponents(this.q, guild, channel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,115 +172,4 @@ export default class UI {
|
|||||||
baseElement.setAttribute('meta-name', name);
|
baseElement.setAttribute('meta-name', name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updatePosition<T>(element: Element, guildCacheMap: Map<string | null, T>, getDirection: ((prevData: T, data: T) => number)) {
|
|
||||||
const data = guildCacheMap.get(element.getAttribute('data-id'));
|
|
||||||
if (!data) {
|
|
||||||
LOG.debug('cache map: ', { guildCacheMap, elementHtml: element.outerHTML })
|
|
||||||
throw new ShouldNeverHappenError('unable to get data from cache map');
|
|
||||||
}
|
|
||||||
// TODO: do-while may be a bit cleaner?
|
|
||||||
let prev = Q.previousElement(element);
|
|
||||||
while (prev != null) {
|
|
||||||
const prevData = guildCacheMap.get(prev.getAttribute('data-id'));
|
|
||||||
if (!prevData) throw new ShouldNeverHappenError('unable to get prevData from cache map');
|
|
||||||
if (getDirection(prevData, data) > 0) { // this element comes before previous element
|
|
||||||
prev.parentElement?.insertBefore(element, prev);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
prev = Q.previousElement(element);
|
|
||||||
}
|
|
||||||
let next = Q.nextElement(element);
|
|
||||||
while (next != null) {
|
|
||||||
const nextData = guildCacheMap.get(next.getAttribute('data-id'));
|
|
||||||
if (!nextData) throw new ShouldNeverHappenError('unable to get nextData from cache map');
|
|
||||||
if (getDirection(data, nextData) > 0) { // this element comes after next element
|
|
||||||
next.parentElement?.insertBefore(next, element);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
next = Q.nextElement(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateChannelPosition(guild: CombinedGuild, channelElement: Element): Promise<void> {
|
|
||||||
this._updatePosition(channelElement, await guild.grabRAMChannelsMap(), (a, b) => {
|
|
||||||
return a.index - b.index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addChannels(guild: CombinedGuild, channels: Channel[], options?: { clear: boolean }): Promise<void> {
|
|
||||||
await this.lockChannels(guild, async () => {
|
|
||||||
if (options?.clear) {
|
|
||||||
Q.clearChildren(this.q.$('#channel-list'));
|
|
||||||
}
|
|
||||||
for (const channel of channels) {
|
|
||||||
const element = createChannel(this.document, this.q, this, guild, channel);
|
|
||||||
this.q.$('#channel-list').appendChild(element);
|
|
||||||
await this.updateChannelPosition(guild, element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteChannels(guild: CombinedGuild, channels: Channel[]): Promise<void> {
|
|
||||||
await this.lockChannels(guild, () => {
|
|
||||||
for (const channel of channels) {
|
|
||||||
const element = this.q.$_('#channel-list .channel[data-id="' + channel.id + '"]');
|
|
||||||
element?.parentElement?.removeChild(element);
|
|
||||||
if (this.activeChannel !== null && this.activeChannel.id == channel.id) {
|
|
||||||
this.activeChannel = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateChannels(guild: CombinedGuild, updatedChannels: Channel[]): Promise<void> {
|
|
||||||
await this.lockChannels(guild, async () => {
|
|
||||||
for (const channel of updatedChannels) {
|
|
||||||
const oldElement = this.q.$('#channel-list .channel[data-id="' + channel.id + '"]');
|
|
||||||
const newElement = createChannel(this.document, this.q, this, guild, channel);
|
|
||||||
oldElement.parentElement?.replaceChild(newElement, oldElement);
|
|
||||||
await this.updateChannelPosition(guild, newElement);
|
|
||||||
|
|
||||||
if (this.activeChannel !== null && this.activeChannel.id === channel.id) {
|
|
||||||
newElement.classList.add('active');
|
|
||||||
|
|
||||||
// See also setActiveChannel
|
|
||||||
this.q.$('#text-input').setAttribute('placeholder', 'Message #' + channel.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setChannels(guild: CombinedGuild, channels: Channel[]): Promise<void> {
|
|
||||||
// check if an element with the same channel and guild exists before adding the new channels
|
|
||||||
// this is nescessary to make sure that if two guilds have channels with the same id, the channel list is still
|
|
||||||
// properly refreshed and the active channel is not improperly set.
|
|
||||||
let oldMatchingElement: HTMLElement | null = null;
|
|
||||||
if (this.activeGuild !== null && this.activeChannel !== null) {
|
|
||||||
oldMatchingElement = this.q.$_('#channel-list .channel[data-id="' + this.activeChannel.id + '"][data-guild-id="' + this.activeGuild.id + '"]');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.addChannels(guild, channels, { clear: true });
|
|
||||||
|
|
||||||
if (this.activeGuild !== null && this.activeGuild.id === guild.id && this.activeChannel !== null) {
|
|
||||||
const newActiveElement = this.q.$_('#channel-list .channel[data-id="' + this.activeChannel.id + '"][data-guild-id="' + this.activeGuild.id + '"]');
|
|
||||||
if (newActiveElement && oldMatchingElement) {
|
|
||||||
const activeChannelId = this.activeChannel.id;
|
|
||||||
const channel = channels.find(channel => channel.id === activeChannelId);
|
|
||||||
if (channel === undefined) throw new ShouldNeverHappenError('current channel does not exist in channels list')
|
|
||||||
this.setActiveChannel(guild, channel);
|
|
||||||
} else {
|
|
||||||
this.activeChannel = null; // the active channel was removed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setChannelsErrorIndicator(guild: CombinedGuild, errorIndicatorElement: Element): Promise<void> {
|
|
||||||
await this.lockChannels(guild, () => {
|
|
||||||
Q.clearChildren(this.q.$('#channel-list'));
|
|
||||||
this.q.$('#channel-list').appendChild(errorIndicatorElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user