partial invites display commit

added control options, fetchable metadata effect, and more
This commit is contained in:
Michael Peters 2021-12-11 14:50:41 -06:00
parent b8c8c45f7b
commit 4b9052a3c5
10 changed files with 186 additions and 66 deletions

View 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;

View File

@ -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>

View File

@ -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;

View File

@ -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} />);
});
}

View File

@ -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;

View File

@ -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}
/>

View File

@ -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')}
/>

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;
}