async button subscription, overlay channel consolidation
This commit is contained in:
parent
965de83433
commit
b5f22212d9
@ -80,6 +80,10 @@ export class Channel implements WithEquals<Channel> {
|
||||
);
|
||||
}
|
||||
|
||||
static sortByIndex(a: Channel, b: Channel) {
|
||||
return a.index - b.index;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.name}#${this.index}`;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ 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';
|
||||
import ChannelOverlay from './overlays/overlay-channel';
|
||||
|
||||
export default function createChannel(document: Document, q: Q, ui: UI, guild: CombinedGuild, channel: Channel) {
|
||||
const element = ReactHelper.createElementFromJSX(
|
||||
@ -38,7 +38,7 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C
|
||||
if (modifyContextElement.parentElement) {
|
||||
modifyContextElement.parentElement.removeChild(modifyContextElement);
|
||||
}
|
||||
ElementsUtil.presentReactOverlay(document, <ModifyChannelOverlay document={document} guild={guild} channel={channel} />);
|
||||
ElementsUtil.presentReactOverlay(document, <ChannelOverlay document={document} guild={guild} channel={channel} />);
|
||||
});
|
||||
|
||||
q.$$$(element, '.modify').addEventListener('mouseenter', () => {
|
||||
|
@ -1,35 +1,21 @@
|
||||
import React, { FC, Ref, useMemo } from 'react';
|
||||
import Button from './button';
|
||||
|
||||
// Includes a submit button and error message
|
||||
|
||||
export interface SubmitOverlayLowerProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
|
||||
buttonMessage: string;
|
||||
submitting: boolean;
|
||||
submitFailed: boolean;
|
||||
shaking: boolean;
|
||||
onSubmit: () => void;
|
||||
errorMessage: string | null;
|
||||
infoMessage?: string | null;
|
||||
|
||||
children: JSX.Element; // buttons list
|
||||
}
|
||||
|
||||
const SubmitOverlayLower: FC<SubmitOverlayLowerProps> = React.forwardRef((props: SubmitOverlayLowerProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { buttonMessage, submitting, submitFailed, shaking, onSubmit, errorMessage, infoMessage } = props;
|
||||
const { errorMessage, infoMessage, children } = props;
|
||||
|
||||
// TODO: ref should be an imperative handle
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
if (submitting) {
|
||||
return 'Submitting...';
|
||||
} else if (submitFailed) {
|
||||
return 'Try Again';
|
||||
} else {
|
||||
return buttonMessage;
|
||||
}
|
||||
}, [ submitting, submitFailed ]);
|
||||
|
||||
const message = useMemo(() => errorMessage ?? infoMessage ?? null, [ errorMessage, infoMessage ]);
|
||||
const isEmpty = useMemo(() => message === null, [ message ]);
|
||||
|
||||
@ -39,7 +25,7 @@ const SubmitOverlayLower: FC<SubmitOverlayLowerProps> = React.forwardRef((props:
|
||||
<div className="error">{errorMessage ?? infoMessage ?? null}</div>
|
||||
</div>
|
||||
<div className="buttons">
|
||||
<Button ref={ref} onClick={onSubmit} shaking={shaking}>{buttonText}</Button>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ import CombinedGuild from '../guild-combined';
|
||||
import React from 'react';
|
||||
import ReactHelper from './require/react-helper';
|
||||
import GuildSettingsOverlay from './overlays/overlay-guild-settings';
|
||||
import CreateChannelOverlay from './overlays/overlay-create-channel';
|
||||
import ChannelOverlay from './overlays/overlay-channel';
|
||||
|
||||
export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): Element {
|
||||
if (ui.activeConnection === null) {
|
||||
@ -81,7 +81,7 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui
|
||||
if (ui.activeConnection.privileges.includes('modify_channels')) {
|
||||
q.$$$(element, '.item.create-channel').addEventListener('click', () => {
|
||||
element.removeSelf();
|
||||
ElementsUtil.presentReactOverlay(document, <CreateChannelOverlay document={document} guild={guild} />);
|
||||
ElementsUtil.presentReactOverlay(document, <ChannelOverlay document={document} guild={guild} />);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FC } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import Display from '../components/display';
|
||||
import InvitePreview from '../components/invite-preview';
|
||||
|
@ -15,8 +15,9 @@ import UI from '../../ui';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import InvitePreview from '../components/invite-preview';
|
||||
import ReactHelper from '../require/react-helper';
|
||||
import ReactHelper, { ExpectedError } from '../require/react-helper';
|
||||
import * as fs from 'fs/promises';
|
||||
import Button from '../components/button';
|
||||
|
||||
export interface IAddGuildData {
|
||||
name: string,
|
||||
@ -59,16 +60,12 @@ export interface AddGuildOverlayProps {
|
||||
const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps) => {
|
||||
const { document, ui, guildsManager, addGuildData } = props;
|
||||
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const expired = addGuildData.expires < new Date().getTime();
|
||||
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
|
||||
const exampleAvatarPath = useMemo(() => getExampleAvatarPath(), []);
|
||||
|
||||
const submitButtonRef = createRef<HTMLDivElement>();
|
||||
|
||||
const [ submitting, setSubmitting ] = useState<boolean>(false);
|
||||
const [ submitFailed, setSubmitFailed ] = useState<boolean>(false);
|
||||
const [ shaking, setShaking ] = useState<boolean>(false);
|
||||
|
||||
const [ displayName, setDisplayName ] = useState<string>(exampleDisplayName);
|
||||
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
@ -92,49 +89,47 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
}
|
||||
}, [ exampleAvatarBuff ]);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
const validationErrorMessage = useMemo(() => {
|
||||
if (exampleAvatarBuffError && !avatarBuff) return 'Unable to load example avatar';
|
||||
if ( !avatarInputValid && avatarInputMessage) return avatarInputMessage;
|
||||
if (!displayNameInputValid && displayNameInputMessage) return displayNameInputMessage;
|
||||
if (addGuildFailedMessage !== null) return addGuildFailedMessage;
|
||||
return null;
|
||||
}, [ exampleAvatarBuffError, avatarBuff, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage, addGuildFailedMessage ]);
|
||||
|
||||
const doSubmit = useCallback(async (): Promise<boolean> => {
|
||||
if (!displayNameInputValid || !avatarInputValid || avatarBuff === null) {
|
||||
return false;
|
||||
}
|
||||
const [ _, submitError, submitButtonText, submitButtonShaking, submitButtonCallback ] = ReactHelper.useAsyncButtonSubscription(async (): Promise<void> => {
|
||||
if (validationErrorMessage || !avatarBuff) throw new ExpectedError('invalid input');
|
||||
|
||||
if (expired) {
|
||||
setAddGuildFailedMessage('token expired');
|
||||
return false;
|
||||
throw new ExpectedError('token expired');
|
||||
}
|
||||
|
||||
let newGuild: CombinedGuild;
|
||||
try {
|
||||
newGuild = await guildsManager.addNewGuild(addGuildData, displayName, avatarBuff);
|
||||
if (!isMounted.current) return;
|
||||
setAddGuildFailedMessage(null);
|
||||
} catch (e: unknown) {
|
||||
if (!isMounted.current) return;
|
||||
LOG.error('error adding new guild', e);
|
||||
if (e instanceof Error) {
|
||||
setAddGuildFailedMessage(e.message);
|
||||
} else {
|
||||
setAddGuildFailedMessage('error adding new guild');
|
||||
}
|
||||
return false;
|
||||
throw e;
|
||||
}
|
||||
|
||||
const guildElement = await ui.addGuild(guildsManager, newGuild);
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
guildElement.click();
|
||||
}, { start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);
|
||||
|
||||
return true;
|
||||
|
||||
}, [ displayName, avatarBuff, displayNameInputValid, avatarInputValid ]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
ElementsUtil.createShakingOnSubmit({ doSubmit, setShaking, setSubmitFailed, setSubmitting }),
|
||||
[ doSubmit, shaking, submitFailed, submitFailed, submitting ]
|
||||
);
|
||||
const errorMessage = useMemo(() => {
|
||||
if (validationErrorMessage) return validationErrorMessage;
|
||||
if (addGuildFailedMessage) return addGuildFailedMessage;
|
||||
return null;
|
||||
}, [ validationErrorMessage, addGuildFailedMessage ])
|
||||
|
||||
return (
|
||||
<div className="content add-guild">
|
||||
@ -157,16 +152,13 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
maxLength={Globals.MAX_DISPLAY_NAME_LENGTH}
|
||||
value={displayName} setValue={setDisplayName}
|
||||
setValid={setDisplayNameInputValid} setMessage={setDisplayNameInputMessage}
|
||||
onEnterKeyDown={() => { submitButtonRef.current?.click(); }}
|
||||
onEnterKeyDown={submitButtonCallback}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitOverlayLower
|
||||
ref={submitButtonRef}
|
||||
buttonMessage="Add Guild"
|
||||
submitting={submitting} submitFailed={submitFailed} shaking={shaking}
|
||||
onSubmit={onSubmit} errorMessage={errorMessage}
|
||||
/>
|
||||
<SubmitOverlayLower errorMessage={errorMessage}>
|
||||
<Button shaking={submitButtonShaking} onClick={submitButtonCallback}>{submitButtonText}</Button>
|
||||
</SubmitOverlayLower>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { createRef, FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { createRef, FC, useEffect, useMemo, useState } from 'react';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import BaseElements from '../require/base-elements';
|
||||
import TextInput from '../components/input-text';
|
||||
@ -11,28 +11,27 @@ import SubmitOverlayLower from '../components/submit-overlay-lower';
|
||||
import Globals from '../../globals';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import { Channel } from '../../data-types';
|
||||
import ReactHelper, { ExpectedError } from '../require/react-helper';
|
||||
import Button from '../components/button';
|
||||
|
||||
export interface ModifyChannelOverlayProps {
|
||||
export interface ChannelOverlayProps {
|
||||
document: Document;
|
||||
guild: CombinedGuild;
|
||||
channel: Channel;
|
||||
channel?: Channel;
|
||||
}
|
||||
const ModifyChannelOverlay: FC<ModifyChannelOverlayProps> = (props: ModifyChannelOverlayProps) => {
|
||||
const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) => {
|
||||
const { document, guild, channel } = props;
|
||||
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const nameInputRef = createRef<HTMLInputElement>();
|
||||
const flavorTextInputRef = createRef<HTMLInputElement>();
|
||||
const submitButtonRef = createRef<HTMLDivElement>();
|
||||
|
||||
const [ edited, setEdited ] = useState<boolean>(false);
|
||||
const [ submitting, setSubmitting ] = useState<boolean>(false);
|
||||
const [ submitFailed, setSubmitFailed ] = useState<boolean>(false);
|
||||
const [ shaking, setShaking ] = useState<boolean>(false);
|
||||
|
||||
const [ name, setName ] = useState<string>(channel.name);
|
||||
const [ flavorText, setFlavorText ] = useState<string>(channel.flavorText ?? '');
|
||||
|
||||
const [ queryFailed, setQueryFailed ] = useState<boolean>(false);
|
||||
const [ name, setName ] = useState<string>(channel?.name ?? '');
|
||||
const [ flavorText, setFlavorText ] = useState<string>(channel?.flavorText ?? '');
|
||||
|
||||
const [ nameInputValid, setNameInputValid ] = useState<boolean>(false);
|
||||
const [ nameInputMessage, setNameInputMessage ] = useState<string | null>(null);
|
||||
@ -40,17 +39,26 @@ const ModifyChannelOverlay: FC<ModifyChannelOverlayProps> = (props: ModifyChanne
|
||||
const [ flavorTextInputValid, setFlavorTextInputValid ] = useState<boolean>(false);
|
||||
const [ flavorTextInputMessage, setFlavorTextInputMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ submitFailMessage, setSubmitFailMessage ] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
nameInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
setEdited(name !== channel.name || ((flavorText === '' ? null : flavorText) !== channel.flavorText));
|
||||
} else {
|
||||
setEdited(name.length > 0 && flavorText.length > 0);
|
||||
}
|
||||
}, [ name, flavorText ]);
|
||||
|
||||
const validationErrorMessage = 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 ]);
|
||||
}, [ nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage ]);
|
||||
|
||||
const infoMessage = useMemo(() => {
|
||||
if ( nameInputValid && nameInputMessage) return nameInputMessage;
|
||||
@ -58,35 +66,37 @@ const ModifyChannelOverlay: FC<ModifyChannelOverlayProps> = (props: ModifyChanne
|
||||
return null;
|
||||
}, [ nameInputValid, nameInputMessage, flavorTextInputValid, flavorTextInputMessage ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (name.length > 0 || flavorText.length > 0) {
|
||||
setEdited(true);
|
||||
}
|
||||
}, [ name, flavorText ]);
|
||||
const [ _, submitError, submitButtonText, submitButtonShaking, submit ] = ReactHelper.useAsyncButtonSubscription(async (): Promise<void> => {
|
||||
if (validationErrorMessage) throw new ExpectedError('invalid input');
|
||||
setSubmitFailMessage(null);
|
||||
|
||||
const doSubmit = useCallback(async (): Promise<boolean> => {
|
||||
LOG.debug('submitting');
|
||||
if (!edited) return false;
|
||||
if (errorMessage) return false;
|
||||
if (!edited) {
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Make sure to null out flavor text if empty
|
||||
await guild.requestDoUpdateChannel(channel.id, name, flavorText === '' ? null : flavorText);
|
||||
if (channel) {
|
||||
await guild.requestDoUpdateChannel(channel.id, name, flavorText === '' ? null : flavorText);
|
||||
} else {
|
||||
await guild.requestDoCreateChannel(name, flavorText === '' ? null : flavorText);
|
||||
}
|
||||
if (!isMounted.current) return;
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error creating channel', e);
|
||||
setQueryFailed(true);
|
||||
return false;
|
||||
if (!isMounted.current) return;
|
||||
setSubmitFailMessage('Error submitting');
|
||||
throw e;
|
||||
}
|
||||
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
}, { start: channel ? 'Modify' : 'Create', pending: 'Submitting...', error: 'Error', done: 'Done' }, [ edited, validationErrorMessage, name, flavorText ]);
|
||||
|
||||
return true;
|
||||
}, [ errorMessage, name, flavorText ]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
ElementsUtil.createShakingOnSubmit({ doSubmit, setShaking, setSubmitFailed, setSubmitting }),
|
||||
[ doSubmit, shaking, submitFailed, submitFailed, submitting ]
|
||||
);
|
||||
const errorMessage = useMemo(() => {
|
||||
if (validationErrorMessage) return validationErrorMessage;
|
||||
if (submitError) return 'Unable to modify channel';
|
||||
return null;
|
||||
}, [ validationErrorMessage, submitError ]);
|
||||
|
||||
return (
|
||||
<div className="content submit-dialog modify-channel">
|
||||
@ -99,7 +109,7 @@ const ModifyChannelOverlay: FC<ModifyChannelOverlayProps> = (props: ModifyChanne
|
||||
<div className="channel-name">
|
||||
<TextInput
|
||||
ref={nameInputRef}
|
||||
label="Channel Name" placeholder={channel.name}
|
||||
label="Channel Name" placeholder={channel?.name ?? 'channel-name'}
|
||||
maxLength={Globals.MAX_CHANNEL_NAME_LENGTH}
|
||||
value={name} setValue={setName}
|
||||
setValid={setNameInputValid} setMessage={setNameInputMessage}
|
||||
@ -110,21 +120,19 @@ const ModifyChannelOverlay: FC<ModifyChannelOverlayProps> = (props: ModifyChanne
|
||||
<div className="flavor-text">
|
||||
<TextInput
|
||||
ref={flavorTextInputRef}
|
||||
label="Flavor Text" placeholder={channel.flavorText ?? '(optional)'}
|
||||
label="Flavor Text" placeholder={channel?.flavorText ?? '(optional)'}
|
||||
maxLength={Globals.MAX_CHANNEL_FLAVOR_TEXT_LENGTH}
|
||||
allowEmpty={true}
|
||||
value={flavorText} setValue={setFlavorText}
|
||||
setValid={setFlavorTextInputValid} setMessage={setFlavorTextInputMessage}
|
||||
onEnterKeyDown={() => { submitButtonRef.current?.click(); }}
|
||||
onEnterKeyDown={submit}
|
||||
/>
|
||||
</div>
|
||||
<SubmitOverlayLower
|
||||
ref={submitButtonRef}
|
||||
submitting={submitting} submitFailed={submitFailed} shaking={shaking}
|
||||
buttonMessage="Create Channel" onSubmit={onSubmit}
|
||||
errorMessage={errorMessage} infoMessage={infoMessage}
|
||||
/>
|
||||
<SubmitOverlayLower errorMessage={errorMessage} infoMessage={infoMessage}>
|
||||
<Button shaking={submitButtonShaking} onClick={submit}>{submitButtonText}</Button>
|
||||
</SubmitOverlayLower>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ModifyChannelOverlay;
|
||||
|
||||
export default ChannelOverlay;
|
@ -1,128 +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 React, { createRef, FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import BaseElements from '../require/base-elements';
|
||||
import TextInput from '../components/input-text';
|
||||
import SubmitOverlayLower from '../components/submit-overlay-lower';
|
||||
import Globals from '../../globals';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
|
||||
export interface CreateChannelOverlayProps {
|
||||
document: Document;
|
||||
guild: CombinedGuild;
|
||||
}
|
||||
const CreateChannelOverlay: FC<CreateChannelOverlayProps> = (props: CreateChannelOverlayProps) => {
|
||||
const { document, guild } = props;
|
||||
|
||||
const nameInputRef = createRef<HTMLInputElement>();
|
||||
const flavorTextInputRef = createRef<HTMLInputElement>();
|
||||
const submitButtonRef = createRef<HTMLDivElement>();
|
||||
|
||||
const [ edited, setEdited ] = useState<boolean>(false);
|
||||
const [ submitting, setSubmitting ] = useState<boolean>(false);
|
||||
const [ submitFailed, setSubmitFailed ] = useState<boolean>(false);
|
||||
const [ shaking, setShaking ] = useState<boolean>(false);
|
||||
|
||||
const [ name, setName ] = useState<string>('');
|
||||
const [ flavorText, setFlavorText ] = useState<string>('');
|
||||
|
||||
const [ queryFailed, setQueryFailed ] = useState<boolean>(false);
|
||||
|
||||
const [ nameInputValid, setNameInputValid ] = useState<boolean>(false);
|
||||
const [ nameInputMessage, setNameInputMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ flavorTextInputValid, setFlavorTextInputValid ] = useState<boolean>(false);
|
||||
const [ flavorTextInputMessage, setFlavorTextInputMessage ] = useState<string | null>(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<boolean> => {
|
||||
LOG.debug('submitting');
|
||||
if (!edited) return false;
|
||||
if (errorMessage) return false;
|
||||
|
||||
try {
|
||||
// Make sure to null out flavor text if empty
|
||||
await guild.requestDoCreateChannel(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 (
|
||||
<div className="content submit-dialog modify-channel">
|
||||
<div className="preview channel-title">
|
||||
<div className="channel-icon">{BaseElements.TEXT_CHANNEL_ICON}</div>
|
||||
<div className="channel-name">{name}</div>
|
||||
<div className="channel-flavor-divider"></div>
|
||||
<div className="channel-flavor-text">{flavorText}</div>
|
||||
</div>
|
||||
<div className="channel-name">
|
||||
<TextInput
|
||||
ref={nameInputRef}
|
||||
label="Channel Name" placeholder="channel-name"
|
||||
maxLength={Globals.MAX_CHANNEL_NAME_LENGTH}
|
||||
value={name} setValue={setName}
|
||||
setValid={setNameInputValid} setMessage={setNameInputMessage}
|
||||
valueMap={value => value.toLowerCase().replace(' ', '-').replace(/[^a-z0-9-]/, '')}
|
||||
onEnterKeyDown={() => flavorTextInputRef.current?.focus()}
|
||||
/>
|
||||
</div>
|
||||
<div className="flavor-text">
|
||||
<TextInput
|
||||
ref={flavorTextInputRef}
|
||||
label="Flavor Text" placeholder="(optional)"
|
||||
maxLength={Globals.MAX_CHANNEL_FLAVOR_TEXT_LENGTH}
|
||||
allowEmpty={true}
|
||||
value={flavorText} setValue={setFlavorText}
|
||||
setValid={setFlavorTextInputValid} setMessage={setFlavorTextInputMessage}
|
||||
onEnterKeyDown={() => { submitButtonRef.current?.click(); }}
|
||||
/>
|
||||
</div>
|
||||
<SubmitOverlayLower
|
||||
ref={submitButtonRef}
|
||||
submitting={submitting} submitFailed={submitFailed} shaking={shaking}
|
||||
buttonMessage="Create Channel" onSubmit={onSubmit}
|
||||
errorMessage={errorMessage} infoMessage={infoMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default CreateChannelOverlay;
|
@ -23,7 +23,6 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guild={guild} />);
|
||||
//if (selectedId === 'channels') setDisplay(<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />);
|
||||
//if (selectedId === 'roles' ) setDisplay(<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />);
|
||||
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guild={guild} />);
|
||||
}, [ selectedId ]);
|
||||
@ -36,7 +35,6 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
|
||||
<div className="content display-swapper guild-settings">
|
||||
<ChoicesControl title={guildNameText} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
|
||||
{ id: 'overview', display: 'Overview' },
|
||||
{ id: 'channels', display: 'Channels' },
|
||||
{ id: 'roles', display: 'Roles' },
|
||||
{ id: 'invites', display: 'Invites' },
|
||||
]} />
|
||||
|
@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { createRef, FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { createRef, FC, useEffect, useMemo, useState } from 'react';
|
||||
import { ConnectionInfo } from '../../data-types';
|
||||
import Globals from '../../globals';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
@ -12,6 +12,8 @@ import TextInput from '../components/input-text';
|
||||
import SubmitOverlayLower from '../components/submit-overlay-lower';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import GuildSubscriptions from '../require/guild-subscriptions';
|
||||
import ReactHelper, { ExpectedError } from '../require/react-helper';
|
||||
import Button from '../components/button';
|
||||
|
||||
export interface PersonalizeOverlayProps {
|
||||
document: Document;
|
||||
@ -25,28 +27,26 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
throw new Error('bad avatar');
|
||||
}
|
||||
|
||||
const isMounted = ReactHelper.useIsMountedRef();
|
||||
|
||||
const [ avatarResource, avatarResourceError ] = GuildSubscriptions.useResourceSubscription(guild, connection.avatarResourceId)
|
||||
|
||||
const displayNameInputRef = createRef<HTMLInputElement>();
|
||||
|
||||
const [ submitting, setSubmitting ] = useState<boolean>(false);
|
||||
const [ submitFailed, setSubmitFailed ] = useState<boolean>(false);
|
||||
const [ shaking, setShaking ] = useState<boolean>(false);
|
||||
|
||||
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(connection.displayName);
|
||||
const [ savedAvatarBuff, setSavedAvatarBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ displayName, setDisplayName ] = useState<string>(connection.displayName);
|
||||
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ saveFailed, setSaveFailed ] = useState<boolean>(false);
|
||||
|
||||
const [ displayNameInputValid, setDisplayNameInputValid ] = useState<boolean>(false);
|
||||
const [ displayNameInputMessage, setDisplayNameInputMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ avatarInputValid, setAvatarInputValid ] = useState<boolean>(false);
|
||||
const [ avatarInputMessage, setAvatarInputMessage ] = useState<string | null>(null);
|
||||
|
||||
const [ submitFailMessage, setSubmitFailMessage ] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatarResource) {
|
||||
if (avatarBuff === savedAvatarBuff) setAvatarBuff(avatarResource.data);
|
||||
@ -58,13 +58,12 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
displayNameInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
const validationErrorMessage = useMemo(() => {
|
||||
if (avatarResourceError) return 'Unable to load avatar';
|
||||
if ( !avatarInputValid && avatarInputMessage) return avatarInputMessage;
|
||||
if (!displayNameInputValid && displayNameInputMessage) return displayNameInputMessage;
|
||||
if (saveFailed) return 'Unable to save personalization';
|
||||
return null;
|
||||
}, [ saveFailed, avatarResourceError, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
|
||||
}, [ avatarResourceError, displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
|
||||
|
||||
const infoMessage = useMemo(() => {
|
||||
if (avatarInputValid && avatarInputMessage) return avatarInputMessage;
|
||||
@ -72,18 +71,20 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
return null;
|
||||
}, [ displayNameInputValid, displayNameInputMessage, avatarInputValid, avatarInputMessage ]);
|
||||
|
||||
const doSubmit = useCallback(async (): Promise<boolean> => {
|
||||
if (errorMessage) return false;
|
||||
const [ _, submitError, submitButtonText, submitButtonShaking, submitButtonCallback ] = ReactHelper.useAsyncButtonSubscription(async (): Promise<void> => {
|
||||
if (validationErrorMessage) throw new ExpectedError('invalid input');
|
||||
setSubmitFailMessage(null);
|
||||
|
||||
if (displayName !== savedDisplayName) {
|
||||
// Save display name
|
||||
try {
|
||||
await guild.requestSetDisplayName(displayName);
|
||||
if (!isMounted.current) return;
|
||||
setSavedDisplayName(displayName);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error setting guild name', e);
|
||||
setSaveFailed(true);
|
||||
return false;
|
||||
if (!isMounted.current) return;
|
||||
setSubmitFailMessage('error setting guild name');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,23 +92,24 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
// Save avatar
|
||||
try {
|
||||
await guild.requestSetAvatar(avatarBuff);
|
||||
if (!isMounted.current) return;
|
||||
setSavedAvatarBuff(avatarBuff);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error setting avatar', e);
|
||||
setSaveFailed(true);
|
||||
return false;
|
||||
if (!isMounted.current) return;
|
||||
setSubmitFailMessage('error setting avatar');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
ElementsUtil.closeReactOverlay(document);
|
||||
}, { start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, [ validationErrorMessage, displayName, savedDisplayName, avatarBuff, savedAvatarBuff ]);
|
||||
|
||||
return true;
|
||||
}, [ errorMessage, displayName, savedDisplayName, avatarBuff, savedAvatarBuff ]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
ElementsUtil.createShakingOnSubmit({ doSubmit, setShaking, setSubmitFailed, setSubmitting }),
|
||||
[ doSubmit, shaking, submitFailed, submitFailed, submitting ]
|
||||
);
|
||||
//if (saveFailed) return 'Unable to save personalization';
|
||||
const errorMessage = useMemo(() => {
|
||||
if (validationErrorMessage) return validationErrorMessage;
|
||||
if (submitFailMessage) return submitFailMessage;
|
||||
return null;
|
||||
}, [ validationErrorMessage, submitError ]);
|
||||
|
||||
return (
|
||||
<div className="content personalize">
|
||||
@ -126,15 +128,13 @@ const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverl
|
||||
maxLength={Globals.MAX_DISPLAY_NAME_LENGTH}
|
||||
value={displayName} setValue={setDisplayName}
|
||||
setValid={setDisplayNameInputValid} setMessage={setDisplayNameInputMessage}
|
||||
onEnterKeyDown={onSubmit}
|
||||
onEnterKeyDown={submitButtonCallback}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubmitOverlayLower
|
||||
submitting={submitting} submitFailed={submitFailed} shaking={shaking}
|
||||
buttonMessage="Save" onSubmit={onSubmit}
|
||||
errorMessage={errorMessage} infoMessage={infoMessage}
|
||||
/>
|
||||
<SubmitOverlayLower errorMessage={errorMessage} infoMessage={infoMessage}>
|
||||
<Button shaking={submitButtonShaking} onClick={submitButtonCallback}>{submitButtonText}</Button>
|
||||
</SubmitOverlayLower>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,5 +2,5 @@
|
||||
@import "./overlay-error-message.scss";
|
||||
@import "./overlay-guild-settings.scss";
|
||||
@import "./overlay-image.scss";
|
||||
@import "./overlay-modify-channel.scss";
|
||||
@import "./overlay-channel.scss";
|
||||
@import "./overlay-personalize.scss";
|
||||
|
@ -11,7 +11,7 @@ import { AutoVerifierChangesType } from "../../auto-verifier";
|
||||
import { Conflictable, Connectable } from "../../guild-types";
|
||||
import { EventEmitter } from 'tsee';
|
||||
import { IDQuery } from '../../auto-verifier-with-args';
|
||||
import { Token } from '../../data-types';
|
||||
import { Token, Channel } from '../../data-types';
|
||||
|
||||
export type SingleSubscriptionEvents = {
|
||||
'fetch': () => void;
|
||||
@ -323,6 +323,23 @@ export default class GuildSubscriptions {
|
||||
}, fetchResourceFunc);
|
||||
}
|
||||
|
||||
static useChannelsSubscription(guild: CombinedGuild) {
|
||||
const fetchChannelsFunc = useCallback(async () => {
|
||||
return await guild.fetchChannels();
|
||||
}, [ guild ]);
|
||||
return GuildSubscriptions.useMultipleGuildSubscription<Channel, 'new-channels', 'update-channels', 'remove-channels', 'conflict-channels'>(guild, {
|
||||
newEventName: 'new-channels',
|
||||
newEventArgsMap: (channels: Channel[]) => channels,
|
||||
updatedEventName: 'update-channels',
|
||||
updatedEventArgsMap: (updatedChannels: Channel[]) => updatedChannels,
|
||||
removedEventName: 'remove-channels',
|
||||
removedEventArgsMap: (removedChannels: Channel[]) => removedChannels,
|
||||
conflictEventName: 'conflict-channels',
|
||||
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Channel>) => changes,
|
||||
sortFunc: Channel.sortByIndex
|
||||
}, fetchChannelsFunc);
|
||||
}
|
||||
|
||||
static useTokensSubscription(guild: CombinedGuild) {
|
||||
const fetchTokensFunc = useCallback(async () => {
|
||||
//LOG.silly('fetching tokens for subscription');
|
||||
|
@ -8,6 +8,8 @@ import ReactDOMServer from "react-dom/server";
|
||||
import { ShouldNeverHappenError } from "../../data-types";
|
||||
import Util from '../../util';
|
||||
|
||||
export class ExpectedError extends Error {}
|
||||
|
||||
// Helper function so we can use JSX before fully committing to React
|
||||
|
||||
export default class ReactHelper {
|
||||
@ -21,6 +23,15 @@ export default class ReactHelper {
|
||||
return document.body.firstElementChild;
|
||||
}
|
||||
|
||||
static useIsMountedRef() {
|
||||
const isMounted = useRef<boolean>(false);
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => { isMounted.current = false; }
|
||||
});
|
||||
return isMounted;
|
||||
}
|
||||
|
||||
static useAsyncActionSubscription<T, V>(
|
||||
actionFunc: () => Promise<T>,
|
||||
initialValue: V,
|
||||
@ -80,6 +91,7 @@ export default class ReactHelper {
|
||||
|
||||
const callback = useCallback(async () => {
|
||||
if (pending) return;
|
||||
setError(null);
|
||||
setPending(true);
|
||||
try {
|
||||
const value = await actionFunc();
|
||||
|
Loading…
Reference in New Issue
Block a user