partial invites display commit
added control options, fetchable metadata effect, and more
This commit is contained in:
parent
b8c8c45f7b
commit
4b9052a3c5
33
src/client/webapp/elements/components/control-choices.tsx
Normal file
33
src/client/webapp/elements/components/control-choices.tsx
Normal file
@ -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<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const ChoicesControl: FC<ChoicesControlProps> = (props: ChoicesControlProps) => {
|
||||
const { title, choices, selectedId, setSelectedId } = props;
|
||||
|
||||
const choiceElements = useMemo(() => {
|
||||
return choices.map(choice => {
|
||||
return (
|
||||
<div
|
||||
key={choice.id}
|
||||
className={'choice' + (selectedId === choice.id ? ' selected' : '')}
|
||||
onClick={() => { setSelectedId(choice.id); }}
|
||||
>{choice.display}</div>
|
||||
);
|
||||
});
|
||||
}, [ choices, selectedId ]);
|
||||
|
||||
return (
|
||||
<div className="choices-react">
|
||||
{title ? <div className="title">{title}</div> : null}
|
||||
{choiceElements}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChoicesControl;
|
@ -10,9 +10,9 @@ const DropdownInput: FC<DropdownInputProps> = (props: DropdownInputProps) => {
|
||||
|
||||
const optionElements = useMemo(() => {
|
||||
return options.map(option => {
|
||||
return <option selected={option.value === value} value={option.value}>{option.display}</option>
|
||||
return <option key={option.value} value={option.value}>{option.display}</option>
|
||||
});
|
||||
}, [ options, value ]);
|
||||
}, [ options ]);
|
||||
|
||||
const onChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
setValue(e.target.value);
|
||||
@ -20,7 +20,7 @@ const DropdownInput: FC<DropdownInputProps> = (props: DropdownInputProps) => {
|
||||
|
||||
return (
|
||||
<div className="dropdown-react">
|
||||
<select onChange={onChange}>
|
||||
<select onChange={onChange} value={value}>
|
||||
{optionElements}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -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<GuildPreviewProps> = (props: GuildPreviewProps) => {
|
||||
const InvitePreview: FC<InvitePreviewProps> = (props: InvitePreviewProps) => {
|
||||
const { name, iconSrc, url, expiresFromNow } = props;
|
||||
|
||||
const expiresText = useMemo(() => {
|
||||
@ -18,8 +18,6 @@ const GuildPreview: FC<GuildPreviewProps> = (props: GuildPreviewProps) => {
|
||||
}
|
||||
}, [ expiresFromNow ]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="guild-preview">
|
||||
<img className="icon" src={iconSrc ?? './img/loading.svg'} alt="icon"></img>
|
||||
@ -32,4 +30,4 @@ const GuildPreview: FC<GuildPreviewProps> = (props: GuildPreviewProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
export default GuildPreview;
|
||||
export default InvitePreview;
|
@ -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, <ErrorMessageOverlay title="Error Opening Settings" message="Could not load guild information" />);
|
||||
} else {
|
||||
ElementsUtil.presentReactOverlay(document, <GuildSettingsOverlay guild={guild} guildMeta={guildMeta} />);
|
||||
}
|
||||
ElementsUtil.presentReactOverlay(document, <GuildSettingsOverlay guild={guild} />);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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<GuildTokensDisplayProps> = (props: GuildTokensDisplayProps) => {
|
||||
const { guild, guildMeta } = props;
|
||||
const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (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<string | null>(null);
|
||||
const [ iconResourceId, setIconResourceId ] = useState<string | null>(null);
|
||||
|
||||
const [ iconSrc, setIconSrc ] = useState<string | null>(null);
|
||||
|
||||
const [ expiresFromNow, setExpiresFromNow ] = useState<Duration>(moment.duration(1, 'day'));
|
||||
const [ expiresFromNow, setExpiresFromNow ] = useState<Duration | null>(moment.duration(1, 'day'));
|
||||
const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('1 day');
|
||||
|
||||
const [ guildMetaFailed, setGuildMetaFailed ] = useState<boolean>(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 (
|
||||
<Display
|
||||
infoMessage={null} errorMessage={null}
|
||||
infoMessage={null} errorMessage={errorMessage}
|
||||
>
|
||||
<div className="section-title">Create Invite</div>
|
||||
<div className="create-invite">
|
||||
<div className="actions">
|
||||
<DropdownInput value={expiresFromNowText} setValue={setExpiresFromNowText} options={[
|
||||
{ value: '1 day', display: 'Expires in 1 Day' },
|
||||
{ value: '1 week', display: 'Expires in 1 Week' },
|
||||
{ value: '1 month', display: 'Expires in 1 Month' },
|
||||
{ value: '1 year', display: 'Expires in 1 Year' },
|
||||
{ value: 'never', display: 'Never expires' },
|
||||
]} />
|
||||
</div>
|
||||
<GuildPreview
|
||||
name={guildMeta.name} iconSrc={iconSrc}
|
||||
<InvitePreview
|
||||
name={guildName ?? ''} iconSrc={iconSrc}
|
||||
url={url} expiresFromNow={expiresFromNow}
|
||||
/>
|
||||
</div>
|
||||
@ -49,4 +86,4 @@ const GuildTokensDisplay: FC<GuildTokensDisplayProps> = (props: GuildTokensDispl
|
||||
)
|
||||
};
|
||||
|
||||
export default GuildTokensDisplay;
|
||||
export default GuildInvitesDisplay;
|
||||
|
@ -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<React.SetStateAction<string>>; // to allow overlay title to update
|
||||
}
|
||||
const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOverviewDisplayProps) => {
|
||||
const { guild, guildMeta } = props;
|
||||
const { guild } = props;
|
||||
|
||||
const [ savedName, setSavedName ] = useState<string>(guildMeta.name);
|
||||
const [ iconResourceId, setIconResourceId ] = useState<string | null>(null);
|
||||
|
||||
const [ savedName, setSavedName ] = useState<string | null>(null);
|
||||
const [ savedIconBuff, setSavedIconBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ name, setName ] = useState<string>(guildMeta.name);
|
||||
const [ name, setName ] = useState<string | null>(null);
|
||||
const [ iconBuff, setIconBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ saving, setSaving ] = useState<boolean>(false);
|
||||
|
||||
const [ guildMetaFailed, setGuildMetaFailed ] = useState<boolean>(false);
|
||||
const [ iconFailed, setIconFailed ] = useState<boolean>(false);
|
||||
const [ saveFailed, setSaveFailed ] = useState<boolean>(false);
|
||||
|
||||
@ -37,11 +40,21 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
const [ iconInputValid, setIconInputValid ] = useState<boolean>(false);
|
||||
const [ iconInputMessage, setIconInputMessage ] = useState<string | null>(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<GuildOverviewDisplayProps> = (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<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
}
|
||||
|
||||
const saveChanges = useCallback(async () => {
|
||||
if (!name || !iconBuff) return;
|
||||
if (errorMessage) return;
|
||||
if (saving) return;
|
||||
|
||||
@ -122,9 +137,9 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
</div>
|
||||
<div className="name">
|
||||
<TextInput
|
||||
label={'Guild Name'} placeholder={savedName}
|
||||
label={'Guild Name'} placeholder={savedName ?? 'Guild Name'}
|
||||
maxLength={Globals.MAX_GUILD_NAME_LENGTH}
|
||||
value={name} setValue={setName}
|
||||
value={name ?? ''} setValue={setName as Dispatch<SetStateAction<string>>}
|
||||
setValid={setNameInputValid} setMessage={setNameInputMessage}
|
||||
onEnterKeyDown={saveChanges}
|
||||
/>
|
||||
|
@ -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<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
|
||||
return (
|
||||
<div className="content add-guild">
|
||||
<GuildPreview
|
||||
<InvitePreview
|
||||
name={addGuildData.name} iconSrc={addGuildData.iconSrc}
|
||||
url={addGuildData.url} expiresFromNow={moment.duration(addGuildData.expires - Date.now(), 'milliseconds')}
|
||||
/>
|
||||
|
@ -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<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
|
||||
const { guild, guildMeta } = props;
|
||||
const { guild } = props;
|
||||
|
||||
const [ guildName, setGuildName ] = useState<string>('');
|
||||
|
||||
const [ selectedId, setSelectedId ] = useState<string>('overview');
|
||||
const [ display, setDisplay ] = useState<JSX.Element>();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guild={guild} setGuildName={setGuildName} />);
|
||||
//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 ]);
|
||||
|
||||
return (
|
||||
<div className="content display-swapper guild-settings">
|
||||
<div className="options">
|
||||
<div className="title">{guildMeta.name}</div>
|
||||
<div className="choosable chosen">Overview</div>
|
||||
<div className="choosable">Channels</div>
|
||||
<div className="choosable">Roles</div>
|
||||
<div className="choosable">Invites</div>
|
||||
</div>
|
||||
<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />
|
||||
<ChoicesControl title={guildName} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
|
||||
{ id: 'overview', display: 'Overview' },
|
||||
{ id: 'channels', display: 'Channels' },
|
||||
{ id: 'roles', display: 'Roles' },
|
||||
{ id: 'invites', display: 'Invites' },
|
||||
]} />
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user