From 4c814881876ba13543e98cf9b9d69bc64f119f89 Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Fri, 10 Dec 2021 01:45:17 -0600 Subject: [PATCH] Reactify modify channel --- src/client/webapp/elements/channel.tsx | 7 +- .../elements/context-menu-guild-title.tsx | 1 - .../elements/overlay-create-channel.tsx | 155 ----------------- .../elements/overlay-modify-channel.tsx | 162 ------------------ .../elements/overlays/overlay-add-guild.tsx | 21 ++- .../overlays/overlay-create-channel.tsx | 2 +- .../overlays/overlay-modify-channel.tsx | 130 ++++++++++++++ src/server/server-controller.ts | 2 +- 8 files changed, 153 insertions(+), 327 deletions(-) delete mode 100644 src/client/webapp/elements/overlay-create-channel.tsx delete mode 100644 src/client/webapp/elements/overlay-modify-channel.tsx create mode 100644 src/client/webapp/elements/overlays/overlay-modify-channel.tsx diff --git a/src/client/webapp/elements/channel.tsx b/src/client/webapp/elements/channel.tsx index f73798b..9641c6c 100644 --- a/src/client/webapp/elements/channel.tsx +++ b/src/client/webapp/elements/channel.tsx @@ -4,11 +4,11 @@ import ReactHelper from './require/react-helper'; import ElementsUtil from './require/elements-util'; import BaseElements from './require/base-elements'; import { Channel } from '../data-types'; -import createModifyChannelOverlay from './overlay-modify-channel'; import UI from '../ui'; import Actions from '../actions'; import Q from '../q-module'; import CombinedGuild from '../guild-combined'; +import ModifyChannelOverlay from './overlays/overlay-modify-channel'; export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) { const element = ReactHelper.createElementFromJSX( @@ -38,10 +38,7 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C if (modifyContextElement.parentElement) { modifyContextElement.parentElement.removeChild(modifyContextElement); } - const modifyOverlay = createModifyChannelOverlay(document, q, guild, channel); - document.body.appendChild(modifyOverlay); - q.$$$(modifyOverlay, '.text-input.channel-name').focus(); - ElementsUtil.setCursorToEnd(q.$$$(modifyOverlay, '.text-input.channel-name')); + ElementsUtil.presentReactOverlay(document, ); }); q.$$$(element, '.modify').addEventListener('mouseenter', () => { diff --git a/src/client/webapp/elements/context-menu-guild-title.tsx b/src/client/webapp/elements/context-menu-guild-title.tsx index 3ac5ebe..8072cfc 100644 --- a/src/client/webapp/elements/context-menu-guild-title.tsx +++ b/src/client/webapp/elements/context-menu-guild-title.tsx @@ -10,7 +10,6 @@ import Q from '../q-module'; import UI from '../ui'; import createCreateInviteTokenOverlay from './overlay-create-invite-token'; -import createCreateChannelOverlay from './overlay-create-channel'; import createTokenLogOverlay from './overlay-token-log'; import CombinedGuild from '../guild-combined'; diff --git a/src/client/webapp/elements/overlay-create-channel.tsx b/src/client/webapp/elements/overlay-create-channel.tsx deleted file mode 100644 index 130cb20..0000000 --- a/src/client/webapp/elements/overlay-create-channel.tsx +++ /dev/null @@ -1,155 +0,0 @@ -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 Globals from "../globals"; - -import ElementsUtil from "./require/elements-util"; -import BaseElements from "./require/base-elements"; -import Q from '../q-module'; -import CombinedGuild from '../guild-combined'; - -import React from 'react'; - -export default function createCreateChannelOverlay(document: Document, q: Q, guild: CombinedGuild): HTMLElement { - - // See also overlay-modify-channel - - const element = BaseElements.createOverlay(document, ( -
-
-
{BaseElements.TEXT_CHANNEL_ICON}
-
channel-name
-
-
-
-
-
-
-
-
-
Create Channel
-
-
-
- )); - - let newName = ''; - let newFlavorText: string | null = ''; - function updatePreview() { - newName = q.$$$(element, '.text-input.channel-name').innerText; - newFlavorText = q.$$$(element, '.text-input.channel-flavor-text').innerText; - q.$$$(element, '.channel-title .channel-name').innerText = newName; - q.$$$(element, '.channel-title .channel-flavor-text').innerText = newFlavorText; - - if (newFlavorText != '') { - q.$$$(element, '.channel-title .channel-flavor-divider').style.visibility = 'visible'; - } else { - q.$$$(element, '.channel-title .channel-flavor-divider').style.visibility = 'hidden'; - } - } - updatePreview(); - - let submitting = false; - async function submit() { - if (submitting) return; - submitting = true; - - q.$$$(element, '.error').innerText = ''; - q.$$$(element, '.button.submit').innerText = 'Submitting...'; - q.$$$(element, '.text-input.channel-name').removeAttribute('contenteditable'); - q.$$$(element, '.text-input.channel-flavor-text').removeAttribute('contenteditable'); - - let success = false; - if (newName.length == 0) { - LOG.warn('attempted to set empty channel name'); - q.$$$(element, '.error').innerText = 'Channel name cannot be empty'; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (newName.length > Globals.MAX_CHANNEL_NAME_LENGTH) { - LOG.warn('attempted to set too long channel name'); - q.$$$(element, '.error').innerText = 'Channel name is too long. ' + newName.length + ' > ' + Globals.MAX_CHANNEL_NAME_LENGTH; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (!(/^[A-Za-z0-9-]+$/.exec(newName))) { - LOG.warn('attempted to set channel name with illegal characters'); - q.$$$(element, '.error').innerText = 'Please use only [A-Za-z0-9-]+ in channel name'; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (newFlavorText != null && newFlavorText.length > Globals.MAX_CHANNEL_FLAVOR_TEXT_LENGTH) { - LOG.warn('attempted to set too long flavor text'); - q.$$$(element, '.error').innerText = 'Flavor text is too long. ' + newName.length + ' > ' + Globals.MAX_CHANNEL_NAME_LENGTH; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else { - if (newFlavorText != null && newFlavorText.length == 0) { - newFlavorText = null; - } - try { - await guild.requestDoCreateChannel(newName, newFlavorText); - success = true; - } catch (e) { - LOG.error('error updating channel', e); - q.$$$(element, '.error').innerText = 'Error updating channel'; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } - } - - if (success) { - element.removeSelf(); - } - - q.$$$(element, '.text-input.channel-name').setAttribute('contenteditable', 'plaintext-only'); - q.$$$(element, '.text-input.channel-flavor-text').setAttribute('contenteditable', 'plaintext-only'); - - submitting = false; - } - - const textInputs = q.$$$$(element, '.text-input'); - for (const textInput of textInputs) { - textInput.addEventListener('input', () => { - updatePreview(); - }); - } - - q.$$$(element, '.text-input.channel-name').addEventListener('keydown', async (e) => { - if (e.key == 'Backspace' || e.key == 'Escape' || e.key == 'F4') { - // these keys are good - } else if (e.key == 'Tab') { - // have to hard-code this one because otherwise, it just picks the beginning of the next input - e.preventDefault(); - e.stopPropagation(); - q.$$$(element, '.text-input.channel-flavor-text').focus(); - ElementsUtil.setCursorToEnd(q.$$$(element, '.text-input.channel-flavor-text')); - } else if (e.key == 'Enter') { - e.preventDefault(); - e.stopPropagation(); - await submit(); - } else if (!/^[A-Za-z0-9-]$/.exec(e.key)) { - e.preventDefault(); - e.stopPropagation(); - } - }); - - q.$$$(element, '.text-input.channel-flavor-text').addEventListener('keydown', async (e) => { - if (e.key == 'Tab' && e.shiftKey) { - // have to hard-code this one because otherwise, it just picks the beginning of the next input - e.preventDefault(); - e.stopPropagation(); - q.$$$(element, '.text-input.channel-name').focus(); - ElementsUtil.setCursorToEnd(q.$$$(element, '.text-input.channel-name')); - } else if (e.key == 'Enter') { - e.preventDefault(); - e.stopPropagation(); - await submit(); - } - }); - - q.$$$(element, '.button.submit').addEventListener('click', async () => { - await submit(); - }); - - return element; -} diff --git a/src/client/webapp/elements/overlay-modify-channel.tsx b/src/client/webapp/elements/overlay-modify-channel.tsx deleted file mode 100644 index 4fe0aee..0000000 --- a/src/client/webapp/elements/overlay-modify-channel.tsx +++ /dev/null @@ -1,162 +0,0 @@ -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 { Channel } from '../data-types'; -import Globals from '../globals.js'; - -import BaseElements, { HTMLElementWithRemoveSelf } from './require/base-elements.js'; -import ElementsUtil from './require/elements-util.js'; -import Q from '../q-module'; -import CombinedGuild from '../guild-combined'; - -import React from 'react'; - -export default function createModifyChannelOverlay(document: Document, q: Q, guild: CombinedGuild, channel: Channel): HTMLElementWithRemoveSelf { - // See also overlay-create-channel - - const element = BaseElements.createOverlay(document, ( -
-
-
{BaseElements.TEXT_CHANNEL_ICON}
-
{channel.name}
-
-
{channel.flavorText || ''}
-
-
-
-
-
-
-
Save Changes
-
-
-
- )); - - q.$$$(element, '.text-input.channel-name').innerText = channel.name; - q.$$$(element, '.text-input.channel-flavor-text').innerText = channel.flavorText || ''; - - let newName = channel.name; - let newFlavorText = channel.flavorText; - function updatePreview() { - newName = q.$$$(element, '.text-input.channel-name').innerText; - newFlavorText = q.$$$(element, '.text-input.channel-flavor-text').innerText; - q.$$$(element, '.channel-title .channel-name').innerText = newName; - q.$$$(element, '.channel-title .channel-flavor-text').innerText = newFlavorText; - - if (newFlavorText != '') { - q.$$$(element, '.channel-title .channel-flavor-divider').style.visibility = 'visible'; - } else { - q.$$$(element, '.channel-title .channel-flavor-divider').style.visibility = 'hidden'; - } - } - updatePreview(); - - let submitting = false; - async function submit() { - if (submitting) return; - submitting = true; - - q.$$$(element, '.error').innerText = ''; - q.$$$(element, '.button.submit').innerText = 'Submitting...'; - q.$$$(element, '.text-input.channel-name').removeAttribute('contenteditable'); - q.$$$(element, '.text-input.channel-flavor-text').removeAttribute('contenteditable'); - - let success = false; - if (newName == channel.name && (newFlavorText || '') == (channel.flavorText || '')) { - success = true; // nothing changed - } else if (newName.length == 0) { - LOG.warn('attempted to set empty channel name'); - q.$$$(element, '.error').innerText = 'Channel name cannot be empty'; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (newName.length > Globals.MAX_CHANNEL_NAME_LENGTH) { - LOG.warn('attempted to set too long channel name'); - q.$$$(element, '.error').innerText = 'Channel name is too long. ' + newName.length + ' > ' + Globals.MAX_CHANNEL_NAME_LENGTH; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (!(/^[A-Za-z0-9-]+$/.exec(newName))) { - LOG.warn('attempted to set channel name with illegal characters'); - q.$$$(element, '.error').innerText = 'Please use only [A-Za-z0-9-]+ in channel name'; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else if (newFlavorText != null && newFlavorText.length > Globals.MAX_CHANNEL_FLAVOR_TEXT_LENGTH) { - LOG.warn('attempted to set too long flavor text'); - q.$$$(element, '.error').innerText = 'Flavor text is too long. ' + newName.length + ' > ' + Globals.MAX_CHANNEL_NAME_LENGTH; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } else { - if (newFlavorText != null && newFlavorText.length == 0) { - newFlavorText = null; - } - try { - await guild.requestDoUpdateChannel(channel.id, newName, newFlavorText); - success = true; - } catch (e) { - LOG.error('error updating channel', e); - q.$$$(element, '.error').innerText = 'Error updating channel'; - q.$$$(element, '.button.submit').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button.submit'), 400); - } - } - - if (success) { - element.removeSelf(); - } - - q.$$$(element, '.text-input.channel-name').setAttribute('contenteditable', 'plaintext-only'); - q.$$$(element, '.text-input.channel-flavor-text').setAttribute('contenteditable', 'plaintext-only'); - - submitting = false; - } - - const textInputs = q.$$$$(element, '.text-input'); - for (const textInput of textInputs) { - textInput.addEventListener('input', () => { - updatePreview(); - }); - } - - q.$$$(element, '.text-input.channel-name').addEventListener('keydown', async (e) => { - if (e.key == 'Backspace' || e.key == 'Escape' || e.key == 'F4') { - // these keys are good - } else if (e.key == 'Tab') { - // have to hard-code this one because otherwise, it just picks the beginning of the next input - e.preventDefault(); - e.stopPropagation(); - q.$$$(element, '.text-input.channel-flavor-text').focus(); - ElementsUtil.setCursorToEnd(q.$$$(element, '.text-input.channel-flavor-text')); - } else if (e.key == 'Enter') { - e.preventDefault(); - e.stopPropagation(); - await submit(); - } else if (!/^[A-Za-z0-9-]$/.exec(e.key)) { - e.preventDefault(); - e.stopPropagation(); - } - }); - - q.$$$(element, '.text-input.channel-flavor-text').addEventListener('keydown', async (e) => { - if (e.key == 'Tab' && e.shiftKey) { - // have to hard-code this one because otherwise, it just picks the beginning of the next input - e.preventDefault(); - e.stopPropagation(); - q.$$$(element, '.text-input.channel-name').focus(); - ElementsUtil.setCursorToEnd(q.$$$(element, '.text-input.channel-name')); - } else if (e.key == 'Enter') { - e.preventDefault(); - e.stopPropagation(); - await submit(); - } - }); - - q.$$$(element, '.button.submit').addEventListener('click', async () => { - await submit(); - }); - - return element; -} diff --git a/src/client/webapp/elements/overlays/overlay-add-guild.tsx b/src/client/webapp/elements/overlays/overlay-add-guild.tsx index 67d8b3b..9edd81e 100644 --- a/src/client/webapp/elements/overlays/overlay-add-guild.tsx +++ b/src/client/webapp/elements/overlays/overlay-add-guild.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, { createRef, FC, useCallback, useEffect, useMemo, useState } from 'react'; import GuildsManager from '../../guilds-manager'; import moment from 'moment'; import TextInput from '../components/input-text'; @@ -62,6 +62,12 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) const exampleDisplayName = useMemo(() => getExampleDisplayName(), []); const exampleAvatarPath = useMemo(() => getExampleAvatarPath(), []); + const submitButtonRef = createRef(); + + const [ submitting, setSubmitting ] = useState(false); + const [ submitFailed, setSubmitFailed ] = useState(false); + const [ shaking, setShaking ] = useState(false); + const [ displayName, setDisplayName ] = useState(exampleDisplayName); const [ avatarBuff, setAvatarBuff ] = useState(null); @@ -133,6 +139,11 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) }, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]); + const onSubmit = useCallback( + ElementsUtil.createShakingOnSubmit({ doSubmit, setShaking, setSubmitFailed, setSubmitting }), + [ doSubmit, shaking, submitFailed, submitFailed, submitting ] + ); + return (
@@ -158,10 +169,16 @@ const AddGuildOverlay: FC = (props: AddGuildOverlayProps) maxLength={Globals.MAX_DISPLAY_NAME_LENGTH} value={displayName} setValue={setDisplayName} setValid={setDisplayNameInputValid} setMessage={setDisplayNameInputMessage} + onEnterKeyDown={() => { submitButtonRef.current?.click(); }} />
- + ); } diff --git a/src/client/webapp/elements/overlays/overlay-create-channel.tsx b/src/client/webapp/elements/overlays/overlay-create-channel.tsx index 05ef993..bee5882 100644 --- a/src/client/webapp/elements/overlays/overlay-create-channel.tsx +++ b/src/client/webapp/elements/overlays/overlay-create-channel.tsx @@ -108,7 +108,7 @@ const CreateChannelOverlay: FC = (props: CreateChanne
= (props: ModifyChannelOverlayProps) => { + const { document, guild, channel } = props; + + const nameInputRef = createRef(); + const flavorTextInputRef = createRef(); + const submitButtonRef = createRef(); + + const [ edited, setEdited ] = useState(false); + const [ submitting, setSubmitting ] = useState(false); + const [ submitFailed, setSubmitFailed ] = useState(false); + const [ shaking, setShaking ] = useState(false); + + const [ name, setName ] = useState(channel.name); + const [ flavorText, setFlavorText ] = useState(channel.flavorText ?? ''); + + const [ queryFailed, setQueryFailed ] = useState(false); + + const [ nameInputValid, setNameInputValid ] = useState(false); + const [ nameInputMessage, setNameInputMessage ] = useState(null); + + const [ flavorTextInputValid, setFlavorTextInputValid ] = useState(false); + const [ flavorTextInputMessage, setFlavorTextInputMessage ] = useState(null); + + useEffect(() => { + nameInputRef.current?.focus(); + }, []); + + const errorMessage = useMemo(() => { + if (!edited) return null; + if ( !nameInputValid && nameInputMessage) return nameInputMessage; + if (!flavorTextInputValid && flavorTextInputMessage) return flavorTextInputMessage; + if (queryFailed) return 'Unable to create new channel'; + return null; + }, [ nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage, queryFailed ]); + + const infoMessage = useMemo(() => { + if ( nameInputValid && nameInputMessage) return nameInputMessage; + if (flavorTextInputValid && flavorTextInputMessage) return flavorTextInputMessage; + return null; + }, [ nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage ]); + + useEffect(() => { + if (name.length > 0 || flavorText.length > 0) { + setEdited(true); + } + }, [ name, flavorText ]); + + const doSubmit = useCallback(async (): Promise => { + LOG.debug('submitting'); + if (!edited) return false; + if (errorMessage) return false; + + try { + // Make sure to null out flavor text if empty + await guild.requestDoUpdateChannel(channel.id, name, flavorText === '' ? null : flavorText); + } catch (e: unknown) { + LOG.error('error creating channel', e); + setQueryFailed(true); + return false; + } + + ElementsUtil.closeReactOverlay(document); + + return true; + }, [ errorMessage, name, flavorText ]); + + const onSubmit = useCallback( + ElementsUtil.createShakingOnSubmit({ doSubmit, setShaking, setSubmitFailed, setSubmitting }), + [ doSubmit, shaking, submitFailed, submitFailed, submitting ] + ); + + return ( +
+
+
{BaseElements.TEXT_CHANNEL_ICON}
+
{name}
+
+
{flavorText}
+
+
+ value.toLowerCase().replace(' ', '-').replace(/[^a-z0-9-]/, '')} + onEnterKeyDown={() => flavorTextInputRef.current?.focus()} + /> +
+
+ { submitButtonRef.current?.click(); }} + /> +
+ +
+ ); +} +export default ModifyChannelOverlay; diff --git a/src/server/server-controller.ts b/src/server/server-controller.ts index 85b323c..0c87bd3 100644 --- a/src/server/server-controller.ts +++ b/src/server/server-controller.ts @@ -398,7 +398,7 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity: bindEvent( client, identity, { verified: true, privileges: [ 'modify_channels' ] }, - 'update-channel', [ 'string', 'string', 'string', 'function' ], + 'update-channel', [ 'string', 'string', 'string?', 'function' ], async (channelId, name, flavorText, respond) => { if (!identity.guildId) throw new EventError('identity no guildId');