diff --git a/.vscode/cordis.code-snippets b/.vscode/cordis.code-snippets new file mode 100644 index 0000000..a3de50b --- /dev/null +++ b/.vscode/cordis.code-snippets @@ -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};", + "" + ] + } +} \ No newline at end of file diff --git a/src/client/webapp/elements/lists/channel-list.scss b/src/client/webapp/elements/lists/channel-list.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/client/webapp/elements/lists/channel-list.tsx b/src/client/webapp/elements/lists/channel-list.tsx new file mode 100644 index 0000000..2f94f3c --- /dev/null +++ b/src/client/webapp/elements/lists/channel-list.tsx @@ -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 = (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(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
Unable to load channels
+ } + return channels?.map((channel: Channel) => { + return ( + { ui.setActiveChannel(guild, channel); setActiveChannel(channel); }} /> + ); + }); + }, [ selfMember, channelsFetchError, channels, guild, selfMember, activeChannel, ui ]); + + return ( +
+ {channelElements} +
+ ); +} + +export default ChannelList; diff --git a/src/client/webapp/elements/lists/components/channel-element.scss b/src/client/webapp/elements/lists/components/channel-element.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/client/webapp/elements/lists/components/channel-element.tsx b/src/client/webapp/elements/lists/components/channel-element.tsx new file mode 100644 index 0000000..d0d3175 --- /dev/null +++ b/src/client/webapp/elements/lists/components/channel-element.tsx @@ -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>; +} + +const ChannelElement: FC = (props: ChannelElementProps) => { + const { guild, channel, activeChannel, setActiveChannel } = props; + + const modifyRef = useRef(null); + + const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text'; + + const setSelfActiveChannel = useCallback((event: MouseEvent) => { + 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, ); + }, [ guild, channel ]); + + return ( +
+
{BaseElements.TEXT_CHANNEL_ICON}
+
{channel.name}
+
{BaseElements.COG}
+
+ ); +} + +export default ChannelElement; diff --git a/src/client/webapp/elements/lists/lists.scss b/src/client/webapp/elements/lists/lists.scss index 200634c..8182f92 100644 --- a/src/client/webapp/elements/lists/lists.scss +++ b/src/client/webapp/elements/lists/lists.scss @@ -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"; diff --git a/src/client/webapp/elements/mounts.tsx b/src/client/webapp/elements/mounts.tsx index 776d417..3d5c5ce 100644 --- a/src/client/webapp/elements/mounts.tsx +++ b/src/client/webapp/elements/mounts.tsx @@ -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'), ); @@ -28,7 +30,8 @@ export function mountGuildComponents(q: Q, guild: CombinedGuild) { ElementsUtil.mountReactComponent(q.$('.member-list-anchor'), ); // channel-list - // TODO + ElementsUtil.unmountReactComponent(q.$('.channel-list-anchor')); + ElementsUtil.mountReactComponent(q.$('.channel-list-anchor'), ); } export function mountGuildChannelComponents(q: Q, guild: CombinedGuild, channel: Channel) { diff --git a/src/client/webapp/index.html b/src/client/webapp/index.html index 8dca8ac..02df39b 100644 --- a/src/client/webapp/index.html +++ b/src/client/webapp/index.html @@ -45,6 +45,7 @@
+
diff --git a/src/client/webapp/styles/channel-list.scss b/src/client/webapp/styles/channel-list.scss index 5c5d2e2..588ea15 100644 --- a/src/client/webapp/styles/channel-list.scss +++ b/src/client/webapp/styles/channel-list.scss @@ -1,6 +1,10 @@ @import "theme.scss"; #channel-list { + display: none; +} + +.channel-list { box-sizing: border-box; padding-top: 8px; overflow-y: scroll; diff --git a/src/client/webapp/ui.ts b/src/client/webapp/ui.ts index bef6f7d..9870f7c 100644 --- a/src/client/webapp/ui.ts +++ b/src/client/webapp/ui.ts @@ -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; }