react text channel list

This commit is contained in:
Michael Peters 2021-12-25 22:10:32 -06:00
parent 2f526c8ecc
commit 6f26181b43
10 changed files with 155 additions and 3 deletions

37
.vscode/cordis.code-snippets vendored Normal file
View File

@ -0,0 +1,37 @@
{
// Place your cordis-ts workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"TSX Base": {
"description": "Generally you'll want this when you create a TSX file",
"prefix": [ "tsx" ],
"body": [
"import React, { FC } from 'react'",
"",
"export interface ${1:Element}Props {",
"\ttext: string;",
"}",
"",
"const ${1:Element}: FC<${1:Element}Props> = (props: ${1:Element}Props) => {",
"\tconst { text } = props;",
"\treturn null;$0",
"}",
"",
"export default ${1:Element};",
""
]
}
}

View File

@ -0,0 +1,58 @@
import React, { FC, useEffect, useMemo, useState } from 'react'
import { Channel } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import UI from '../../ui';
import Util from '../../util';
import GuildSubscriptions from '../require/guild-subscriptions';
import ReactHelper from '../require/react-helper';
import ChannelElement from './components/channel-element';
export interface ChannelListProps {
guild: CombinedGuild;
ui: UI; // TODO: Remove dependency on UI
}
const ChannelList: FC<ChannelListProps> = (props: ChannelListProps) => {
const { guild, ui } = props;
// TODO: Retry button on error
// TODO: Load selfMember as a property so its state can be stored higher up and re-used
const [ selfMember ] = GuildSubscriptions.useSelfMemberSubscription(guild);
const [ fetchRetryCallable, channels, channelsFetchError ] = GuildSubscriptions.useChannelsSubscription(guild);
const [ activeChannel, setActiveChannel ] = useState<Channel | null>(ui.activeChannel);
// This chunk is aids but it'll be gotten rid of soon when we make the whole guild section one big component.
// DO NOT COPY THIS ANYWHERE ELSE
const isMounted = ReactHelper.useIsMountedRef();
useEffect(() => {
(async () => {
await Util.sleep(100);
if (!isMounted.current) return;
setActiveChannel(ui.activeChannel);
})();
}, [ ui ]);
const channelElements = useMemo(() => {
if (!selfMember) return null; // TODO: Hopefully selfMember will be non-null in the future
if (channelsFetchError) {
// TODO: Try again
return <div className="channels-failed">Unable to load channels</div>
}
return channels?.map((channel: Channel) => {
return (
<ChannelElement
key={channel.id} guild={guild} channel={channel}
selfMember={selfMember} activeChannel={activeChannel} setActiveChannel={() => { ui.setActiveChannel(guild, channel); setActiveChannel(channel); }} />
);
});
}, [ selfMember, channelsFetchError, channels, guild, selfMember, activeChannel, ui ]);
return (
<div className="channel-list">
{channelElements}
</div>
);
}
export default ChannelList;

View File

@ -0,0 +1,47 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, MouseEvent, SetStateAction, useCallback, useRef } from 'react'
import { Channel, Member } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
import ChannelOverlay from '../../overlays/overlay-channel';
import BaseElements from '../../require/base-elements';
import ElementsUtil from '../../require/elements-util';
export interface ChannelElementProps {
guild: CombinedGuild;
channel: Channel;
selfMember: Member; // Note: Expected to use this later since it may not be best to have css-based hiding
activeChannel: Channel | null;
setActiveChannel: Dispatch<SetStateAction<Channel | null>>;
}
const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) => {
const { guild, channel, activeChannel, setActiveChannel } = props;
const modifyRef = useRef<HTMLDivElement>(null);
const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text';
const setSelfActiveChannel = useCallback((event: MouseEvent<HTMLDivElement>) => {
if (modifyRef.current?.contains(event.target as Node)) return; // ignore "modify" button clicks
setActiveChannel(channel);
}, [ modifyRef, channel ]);
// Note: this element will be hidden by CSS
const launchModify = useCallback(() => {
ElementsUtil.presentReactOverlay(document, <ChannelOverlay guild={guild} channel={channel} />);
}, [ guild, channel ]);
return (
<div className={baseClassName} onClick={setSelfActiveChannel}>
<div className="icon">{BaseElements.TEXT_CHANNEL_ICON}</div>
<div className="name">{channel.name}</div>
<div className="modify" ref={modifyRef} onClick={launchModify}>{BaseElements.COG}</div>
</div>
);
}
export default ChannelElement;

View File

@ -1,5 +1,7 @@
@import "./channel-list.scss";
@import "./member-list.scss";
@import "./message-list.scss";
@import "./components/channel-element.scss";
@import "./components/member-element.scss";
@import "./components/message-element.scss";

View File

@ -8,13 +8,15 @@ import MessageList from './lists/message-list';
import ChannelTitle from './sections/channel-title';
import ConnectionInfo from './sections/connection-info';
import GuildTitle from './sections/guild-title';
import UI from '../ui';
import ChannelList from './lists/channel-list';
export function mountBaseComponents() {
// guild-list
// TODO
}
export function mountGuildComponents(q: Q, guild: CombinedGuild) {
export function mountGuildComponents(q: Q, ui: UI, guild: CombinedGuild) {
// guild title
ElementsUtil.unmountReactComponent(q.$('.guild-title-anchor'));
ElementsUtil.mountReactComponent(q.$('.guild-title-anchor'), <GuildTitle guild={guild} />);
@ -28,7 +30,8 @@ export function mountGuildComponents(q: Q, guild: CombinedGuild) {
ElementsUtil.mountReactComponent(q.$('.member-list-anchor'), <MemberList guild={guild} />);
// channel-list
// TODO
ElementsUtil.unmountReactComponent(q.$('.channel-list-anchor'));
ElementsUtil.mountReactComponent(q.$('.channel-list-anchor'), <ChannelList guild={guild} ui={ui} />);
}
export function mountGuildChannelComponents(q: Q, guild: CombinedGuild, channel: Channel) {

View File

@ -45,6 +45,7 @@
<div id="guild">
<div id="guild-sidebar">
<div class="guild-title-anchor"></div>
<div class="channel-list-anchor"></div>
<div id="channel-list"></div>
<div class="connection-anchor"></div>
</div>

View File

@ -1,6 +1,10 @@
@import "theme.scss";
#channel-list {
display: none;
}
.channel-list {
box-sizing: border-box;
padding-top: 8px;
overflow-y: scroll;

View File

@ -107,7 +107,7 @@ export default class UI {
next.classList.add('active');
this.q.$('#guild').setAttribute('data-id', guild.id + '');
mountGuildComponents(this.q, guild);
mountGuildComponents(this.q, this, guild);
this.activeGuild = guild;
}