From 4b9052a3c50c9bece5f76a131b9b6e08fe4c419a Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sat, 11 Dec 2021 14:50:41 -0600 Subject: [PATCH] partial invites display commit added control options, fetchable metadata effect, and more --- .../elements/components/control-choices.tsx | 33 ++++++++++ .../elements/components/input-dropdown.tsx | 6 +- .../{guild-preview.tsx => invite-preview.tsx} | 10 ++- .../elements/context-menu-guild-title.tsx | 12 +--- .../displays/display-guild-invites.tsx | 63 +++++++++++++++---- .../displays/display-guild-overview.tsx | 37 +++++++---- .../elements/overlays/overlay-add-guild.tsx | 6 +- .../overlays/overlay-guild-settings.tsx | 34 ++++++---- .../webapp/elements/require/react-helper.ts | 43 ++++++++++++- src/client/webapp/styles/overlays.scss | 8 +-- 10 files changed, 186 insertions(+), 66 deletions(-) create mode 100644 src/client/webapp/elements/components/control-choices.tsx rename src/client/webapp/elements/components/{guild-preview.tsx => invite-preview.tsx} (80%) diff --git a/src/client/webapp/elements/components/control-choices.tsx b/src/client/webapp/elements/components/control-choices.tsx new file mode 100644 index 0000000..81ebd4d --- /dev/null +++ b/src/client/webapp/elements/components/control-choices.tsx @@ -0,0 +1,33 @@ +import React, { FC, useMemo } from 'react'; + +export interface ChoicesControlProps { + title?: string; + choices: { id: string, display: string }[] + selectedId: string; + setSelectedId: React.Dispatch>; +} + +const ChoicesControl: FC = (props: ChoicesControlProps) => { + const { title, choices, selectedId, setSelectedId } = props; + + const choiceElements = useMemo(() => { + return choices.map(choice => { + return ( +
{ setSelectedId(choice.id); }} + >{choice.display}
+ ); + }); + }, [ choices, selectedId ]); + + return ( +
+ {title ?
{title}
: null} + {choiceElements} +
+ ); +}; + +export default ChoicesControl; \ No newline at end of file diff --git a/src/client/webapp/elements/components/input-dropdown.tsx b/src/client/webapp/elements/components/input-dropdown.tsx index d8af4ac..8e465b1 100644 --- a/src/client/webapp/elements/components/input-dropdown.tsx +++ b/src/client/webapp/elements/components/input-dropdown.tsx @@ -10,9 +10,9 @@ const DropdownInput: FC = (props: DropdownInputProps) => { const optionElements = useMemo(() => { return options.map(option => { - return + return }); - }, [ options, value ]); + }, [ options ]); const onChange = (e: ChangeEvent) => { setValue(e.target.value); @@ -20,7 +20,7 @@ const DropdownInput: FC = (props: DropdownInputProps) => { return (
- {optionElements}
diff --git a/src/client/webapp/elements/components/guild-preview.tsx b/src/client/webapp/elements/components/invite-preview.tsx similarity index 80% rename from src/client/webapp/elements/components/guild-preview.tsx rename to src/client/webapp/elements/components/invite-preview.tsx index 3956c47..0d1021f 100644 --- a/src/client/webapp/elements/components/guild-preview.tsx +++ b/src/client/webapp/elements/components/invite-preview.tsx @@ -1,13 +1,13 @@ import moment, { Duration } from 'moment'; import React, { FC, useMemo } from 'react'; -export interface GuildPreviewProps { +export interface InvitePreviewProps { name: string; iconSrc: string | null; url: string; - expiresFromNow?: Duration; + expiresFromNow: Duration | null; } -const GuildPreview: FC = (props: GuildPreviewProps) => { +const InvitePreview: FC = (props: InvitePreviewProps) => { const { name, iconSrc, url, expiresFromNow } = props; const expiresText = useMemo(() => { @@ -18,8 +18,6 @@ const GuildPreview: FC = (props: GuildPreviewProps) => { } }, [ expiresFromNow ]); - - return (
icon @@ -32,4 +30,4 @@ const GuildPreview: FC = (props: GuildPreviewProps) => { ); } -export default GuildPreview; \ No newline at end of file +export default InvitePreview; \ No newline at end of file diff --git a/src/client/webapp/elements/context-menu-guild-title.tsx b/src/client/webapp/elements/context-menu-guild-title.tsx index 8072cfc..d02b912 100644 --- a/src/client/webapp/elements/context-menu-guild-title.tsx +++ b/src/client/webapp/elements/context-menu-guild-title.tsx @@ -76,17 +76,7 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui if (ui.activeConnection.privileges.includes('modify_profile')) { q.$$$(element, '.item.guild-settings').addEventListener('click', async () => { element.removeSelf(); - let guildMeta: GuildMetadata | null = null; - try { - guildMeta = await guild.fetchMetadata(); - } catch (e) { - LOG.error('error fetching guild info', e); - } - if (guildMeta === null) { - ElementsUtil.presentReactOverlay(document, ); - } else { - ElementsUtil.presentReactOverlay(document, ); - } + ElementsUtil.presentReactOverlay(document, ); }); } diff --git a/src/client/webapp/elements/displays/display-guild-invites.tsx b/src/client/webapp/elements/displays/display-guild-invites.tsx index 625bf4f..27968dc 100644 --- a/src/client/webapp/elements/displays/display-guild-invites.tsx +++ b/src/client/webapp/elements/displays/display-guild-invites.tsx @@ -3,45 +3,82 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { FC } from 'react'; import CombinedGuild from '../../guild-combined'; import Display from '../components/display'; -import GuildPreview from '../components/guild-preview'; +import InvitePreview from '../components/invite-preview'; import { GuildMetadata } from '../../data-types'; import ReactHelper from '../require/react-helper'; import { Duration } from 'moment'; import moment from 'moment'; +import DropdownInput from '../components/input-dropdown'; -export interface GuildTokensDisplayProps { +export interface GuildInvitesDisplayProps { guild: CombinedGuild; - guildMeta: GuildMetadata; } -const GuildTokensDisplay: FC = (props: GuildTokensDisplayProps) => { - const { guild, guildMeta } = props; +const GuildInvitesDisplay: FC = (props: GuildInvitesDisplayProps) => { + const { guild } = props; - const url = 'https://localhost:3030'; // TODO + const url = 'https://localhost:3030'; // TODO: this will likely be a dropdown list at some point + + const [ guildName, setGuildName ] = useState(null); + const [ iconResourceId, setIconResourceId ] = useState(null); const [ iconSrc, setIconSrc ] = useState(null); - const [ expiresFromNow, setExpiresFromNow ] = useState(moment.duration(1, 'day')); + const [ expiresFromNow, setExpiresFromNow ] = useState(moment.duration(1, 'day')); + const [ expiresFromNowText, setExpiresFromNowText ] = useState('1 day'); + + const [ guildMetaFailed, setGuildMetaFailed ] = useState(false); + + useEffect(() => { + if (expiresFromNowText === 'never') { + setExpiresFromNow(null); + return; + } else { + const splt = expiresFromNowText.split(' '); + setExpiresFromNow(moment.duration(splt[0], splt[1] as moment.unitOfTime.DurationConstructor)); + } + }, [ expiresFromNowText ]); + + ReactHelper.useGuildMetadataEffect({ + guild, + onSuccess: (guildMeta: GuildMetadata) => { + setGuildName(guildMeta.name); + setIconResourceId(guildMeta.iconResourceId); + }, + onError: () => setGuildMetaFailed(true) + }); ReactHelper.useSoftImageSrcResourceEffect({ - guild, resourceId: guildMeta.iconResourceId, + guild, resourceId: iconResourceId, onSuccess: setIconSrc }); + const errorMessage = useMemo(() => { + if (guildMetaFailed) return 'Unable to load guild metadata'; + return null; + }, [ guildMetaFailed ]); + return (
Create Invite
+
-
@@ -49,4 +86,4 @@ const GuildTokensDisplay: FC = (props: GuildTokensDispl ) }; -export default GuildTokensDisplay; \ No newline at end of file +export default GuildInvitesDisplay; diff --git a/src/client/webapp/elements/displays/display-guild-overview.tsx b/src/client/webapp/elements/displays/display-guild-overview.tsx index 61645ef..6d77584 100644 --- a/src/client/webapp/elements/displays/display-guild-overview.tsx +++ b/src/client/webapp/elements/displays/display-guild-overview.tsx @@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { GuildMetadata } from '../../data-types'; import Globals from '../../globals'; @@ -15,19 +15,22 @@ import ReactHelper from '../require/react-helper'; export interface GuildOverviewDisplayProps { guild: CombinedGuild; - guildMeta: GuildMetadata; + setGuildName: React.Dispatch>; // to allow overlay title to update } const GuildOverviewDisplay: FC = (props: GuildOverviewDisplayProps) => { - const { guild, guildMeta } = props; + const { guild } = props; - const [ savedName, setSavedName ] = useState(guildMeta.name); + const [ iconResourceId, setIconResourceId ] = useState(null); + + const [ savedName, setSavedName ] = useState(null); const [ savedIconBuff, setSavedIconBuff ] = useState(null); - const [ name, setName ] = useState(guildMeta.name); + const [ name, setName ] = useState(null); const [ iconBuff, setIconBuff ] = useState(null); const [ saving, setSaving ] = useState(false); + const [ guildMetaFailed, setGuildMetaFailed ] = useState(false); const [ iconFailed, setIconFailed ] = useState(false); const [ saveFailed, setSaveFailed ] = useState(false); @@ -37,11 +40,21 @@ const GuildOverviewDisplay: FC = (props: GuildOvervie const [ iconInputValid, setIconInputValid ] = useState(false); const [ iconInputMessage, setIconInputMessage ] = useState(null); - ReactHelper.useResourceEffect({ - guild, resourceId: guildMeta.iconResourceId, + ReactHelper.useGuildMetadataEffect({ + guild, + onSuccess: (guildMeta: GuildMetadata) => { + setSavedName(guildMeta.name); + setName(guildMeta.name); + setIconResourceId(guildMeta.iconResourceId); + }, + onError: () => setGuildMetaFailed(true) + }) + + ReactHelper.useNullableResourceEffect({ + guild, resourceId: iconResourceId, onSuccess: (resource) => { - setSavedIconBuff(resource.data); - setIconBuff(resource.data); + setSavedIconBuff(resource?.data ?? null); + setIconBuff(resource?.data ?? null); }, onError: () => { setIconFailed(true); @@ -53,6 +66,7 @@ const GuildOverviewDisplay: FC = (props: GuildOvervie }, [ name, savedName, iconBuff, savedIconBuff ]); const errorMessage = useMemo(() => { + if (guildMetaFailed) return 'Unable to load guild metadata'; if (iconFailed) return 'Unable to load icon'; if (!iconInputValid && iconInputMessage) return iconInputMessage; if (!nameInputValid && nameInputMessage) return nameInputMessage; @@ -71,6 +85,7 @@ const GuildOverviewDisplay: FC = (props: GuildOvervie } const saveChanges = useCallback(async () => { + if (!name || !iconBuff) return; if (errorMessage) return; if (saving) return; @@ -122,9 +137,9 @@ const GuildOverviewDisplay: FC = (props: GuildOvervie
>} setValid={setNameInputValid} setMessage={setNameInputMessage} onEnterKeyDown={saveChanges} /> diff --git a/src/client/webapp/elements/overlays/overlay-add-guild.tsx b/src/client/webapp/elements/overlays/overlay-add-guild.tsx index 9ffff33..2fca21e 100644 --- a/src/client/webapp/elements/overlays/overlay-add-guild.tsx +++ b/src/client/webapp/elements/overlays/overlay-add-guild.tsx @@ -11,12 +11,10 @@ import ImageEditInput from '../components/input-image-edit'; import Globals from '../../globals'; import SubmitOverlayLower from '../components/submit-overlay-lower'; import path from 'path'; -import fs from 'fs/promises'; -import Util from '../../util'; import UI from '../../ui'; import CombinedGuild from '../../guild-combined'; import ElementsUtil from '../require/elements-util'; -import GuildPreview from '../components/guild-preview'; +import InvitePreview from '../components/invite-preview'; import ReactHelper from '../require/react-helper'; export interface IAddGuildData { @@ -134,7 +132,7 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) return (
- diff --git a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx index 1dca506..0c7de6e 100644 --- a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx +++ b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx @@ -1,25 +1,37 @@ -import React, { FC, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; import { GuildMetadata } from "../../data-types"; import CombinedGuild from "../../guild-combined"; +import ChoicesControl from "../components/control-choices"; +import GuildInvitesDisplay from "../displays/display-guild-invites"; import GuildOverviewDisplay from "../displays/display-guild-overview"; export interface GuildSettingsOverlayProps { guild: CombinedGuild; - guildMeta: GuildMetadata; } const GuildSettingsOverlay: FC = (props: GuildSettingsOverlayProps) => { - const { guild, guildMeta } = props; + const { guild } = props; + + const [ guildName, setGuildName ] = useState(''); + + const [ selectedId, setSelectedId ] = useState('overview'); + const [ display, setDisplay ] = useState(); + + useEffect(() => { + if (selectedId === 'overview') setDisplay(); + //if (selectedId === 'channels') setDisplay(); + //if (selectedId === 'roles' ) setDisplay(); + if (selectedId === 'invites' ) setDisplay(); + }, [ selectedId ]); return (
-
-
{guildMeta.name}
-
Overview
-
Channels
-
Roles
-
Invites
-
- + + {display}
); } diff --git a/src/client/webapp/elements/require/react-helper.ts b/src/client/webapp/elements/require/react-helper.ts index 01842c8..9b814f7 100644 --- a/src/client/webapp/elements/require/react-helper.ts +++ b/src/client/webapp/elements/require/react-helper.ts @@ -5,7 +5,7 @@ const LOG = Logger.create(__filename, electronConsole); import { DependencyList, useEffect } from "react"; import ReactDOMServer from "react-dom/server"; -import { Resource, ShouldNeverHappenError } from "../../data-types"; +import { GuildMetadata, Resource, ShouldNeverHappenError } from "../../data-types"; import CombinedGuild from "../../guild-combined"; import ElementsUtil from './elements-util'; import * as fs from 'fs/promises'; @@ -42,6 +42,23 @@ export default class ReactHelper { }, deps); } + static useGuildMetadataEffect(params: { + guild: CombinedGuild, + onSuccess: (guildMeta: GuildMetadata) => void, + onError: () => void, + }): void { + const { guild, onSuccess, onError } = params; + + ReactHelper.useCustomAsyncEffect({ + actionFunc: async () => { + return await guild.fetchMetadata(); + }, + onSuccess: (value) => onSuccess(value), + onError: (e) => { LOG.error('unable to load guild metadata', e); onError() }, + deps: [ guild ] + }); + } + static useBufferFileEffect(params: { filePath: string, onSuccess: (buff: Buffer) => void, @@ -60,6 +77,25 @@ export default class ReactHelper { }); } + static useNullableResourceEffect(params: { + guild: CombinedGuild, resourceId: string | null, + onSuccess: (resource: Resource | null) => void, + onError: () => void + }): void { + const { guild, resourceId, onSuccess, onError } = params; + + ReactHelper.useCustomAsyncEffect({ + actionFunc: async () => { + if (resourceId === null) return null; + const resource = await guild.fetchResource(resourceId); + return resource; + }, + onSuccess: (value) => onSuccess(value), + onError: (e) => { LOG.error('unable to fetch resource', e); onError(); }, + deps: [ guild, resourceId ] + }); + } + static useResourceEffect(params: { guild: CombinedGuild, resourceId: string, onSuccess: (resource: Resource) => void, @@ -79,7 +115,7 @@ export default class ReactHelper { } static useImageSrcResourceEffect(params: { - guild: CombinedGuild, resourceId: string, + guild: CombinedGuild, resourceId: string | null, onSuccess: (imgSrc: string) => void, onError: () => void }): void { @@ -87,6 +123,7 @@ export default class ReactHelper { ReactHelper.useCustomAsyncEffect({ actionFunc: async () => { + if (resourceId === null) return './img/loading.svg'; const resource = await guild.fetchResource(resourceId); const imgSrc = await ElementsUtil.getImageBufferSrc(resource.data); return imgSrc; @@ -98,7 +135,7 @@ export default class ReactHelper { } static useSoftImageSrcResourceEffect(params: { - guild: CombinedGuild, resourceId: string, + guild: CombinedGuild, resourceId: string | null, onSuccess: (imgSrc: string) => void }): void { const { guild, resourceId, onSuccess } = params; diff --git a/src/client/webapp/styles/overlays.scss b/src/client/webapp/styles/overlays.scss index 7867fe0..73f0027 100644 --- a/src/client/webapp/styles/overlays.scss +++ b/src/client/webapp/styles/overlays.scss @@ -268,7 +268,7 @@ body > .overlay, $content-border-radius: 4px; - > .options { + > .choices-react { box-sizing: border-box; background-color: $background-secondary; width: 226px; @@ -285,19 +285,19 @@ body > .overlay, margin-bottom: 4px; } - > .choosable { + > .choice { color: $interactive-normal; cursor: pointer; padding: 6px 12px; border-radius: 3px; margin-bottom: 4px; - &.chosen { + &.selected { color: $interactive-active; background-color: $background-modifier-selected } - &:not(.chosen):hover { + &:not(.selected):hover { color: $interactive-hover; background-color: $background-modifier-hover; }