create invite token dialog

This commit is contained in:
Michael Peters 2021-12-13 02:29:06 -06:00
parent d875a8f8fb
commit 84d0a0e542
9 changed files with 851 additions and 757 deletions

View File

@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger'; import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole); const LOG = Logger.create(__filename, electronConsole);
import React, { useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { FC } from 'react'; import { FC } from 'react';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
import Display from '../components/display'; import Display from '../components/display';
@ -49,8 +49,19 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
} }
}, [ expiresFromNowText ]); }, [ expiresFromNowText ]);
const [ tokenResult, tokenError, tokenButtonText, tokenButtonShaking, tokenButtonCallback ] = ReactHelper.useAsyncButtonSubscription(
async () => await guild.requestDoCreateToken(expiresFromNowText === 'never' ? null : expiresFromNowText),
{ start: 'Create Token', pending: 'Creating...', error: 'Try Again', done: 'Create Token' },
[ guild, expiresFromNowText ]
);
const createToken = useCallback(async () => {
await guild.requestDoCreateToken(expiresFromNowText); // note: the text, NOT the duration. The server uses PostgreSQL interval conversion
}, [ expiresFromNowText ]);
const errorMessage = useMemo(() => { const errorMessage = useMemo(() => {
if (guildMetaError) return 'Unable to load guild metadata'; if (guildMetaError) return 'Unable to load guild metadata';
if (tokenError) return 'Unable to create token';
return null; return null;
}, [ guildMetaError ]); }, [ guildMetaError ]);
@ -97,7 +108,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
{ value: '1 month', display: 'a Month' }, { value: '1 month', display: 'a Month' },
{ value: 'never', display: 'Never' }, { value: 'never', display: 'Never' },
]} /> ]} />
<div><Button>Create Invite</Button></div> <div><Button shaking={tokenButtonShaking} onClick={tokenButtonCallback}>{tokenButtonText}</Button></div>
</div> </div>
<InvitePreview <InvitePreview
name={guildMeta?.name ?? ''} iconSrc={iconSrc} name={guildMeta?.name ?? ''} iconSrc={iconSrc}

View File

@ -205,6 +205,7 @@ export default class GuildSubscriptions {
const events = useMemo(() => new EventEmitter<MultipleSubscriptionEvents<T>>(), []); const events = useMemo(() => new EventEmitter<MultipleSubscriptionEvents<T>>(), []);
const onFetch = useCallback((fetchValue: T[] | null) => { const onFetch = useCallback((fetchValue: T[] | null) => {
if (fetchValue) fetchValue.sort(sortFunc);
setValue(fetchValue); setValue(fetchValue);
setFetchError(null); setFetchError(null);
events.emit('fetch'); events.emit('fetch');

View File

@ -3,9 +3,10 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger'; import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole); const LOG = Logger.create(__filename, electronConsole);
import { DependencyList, useEffect, useRef, useState } from "react"; import { DependencyList, useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { ShouldNeverHappenError } from "../../data-types"; import { ShouldNeverHappenError } from "../../data-types";
import Util from '../../util';
// Helper function so we can use JSX before fully committing to React // Helper function so we can use JSX before fully committing to React
@ -51,4 +52,54 @@ export default class ReactHelper {
return [ value, error ]; return [ value, error ];
} }
static useAsyncButtonSubscription<T>(
actionFunc: () => Promise<T>,
stateText: { start: string, pending: string, error: string, done: string },
deps: DependencyList
): [ result: T | null, error: unknown | null, text: string, shaking: boolean, callback: () => void ] {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => { isMounted.current = false; }
});
const [ result, setResult ] = useState<T | null>(null);
const [ error, setError ] = useState<unknown | null>(null);
const [ pending, setPending ] = useState<boolean>(false);
const [ complete, setComplete ] = useState<boolean>(false);
const [ shaking, setShaking ] = useState<boolean>(false);
const text = useMemo(() => {
if (error) return stateText.error;
if (pending) return stateText.pending;
if (complete) return stateText.done;
return stateText.start;
}, [ error, pending, complete ]);
const callback = useCallback(async () => {
if (pending) return;
setPending(true);
try {
const value = await actionFunc();
if (!isMounted.current) return;
setResult(value);
setComplete(true);
setError(null);
setPending(false);
} catch (e: unknown) {
LOG.error('unable to perform async button subscription');
if (!isMounted.current) return;
setError(e);
setShaking(true);
await Util.sleep(400);
if (!isMounted.current) return;
setShaking(false);
setPending(false);
}
}, [ ...deps, pending ]);
return [ result, error, text, shaking, callback ];
}
} }

View File

@ -384,13 +384,16 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
async requestSetGuildIcon(guildIcon: Buffer): Promise<void> { async requestSetGuildIcon(guildIcon: Buffer): Promise<void> {
await this.socketGuild.requestSetGuildIcon(guildIcon); await this.socketGuild.requestSetGuildIcon(guildIcon);
} }
async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void> { async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<Channel> {
await this.socketGuild.requestDoUpdateChannel(channelId, name, flavorText); return await this.socketGuild.requestDoUpdateChannel(channelId, name, flavorText);
} }
async requestDoCreateChannel(name: string, flavorText: string | null): Promise<void> { async requestDoCreateChannel(name: string, flavorText: string | null): Promise<Channel> {
await this.socketGuild.requestDoCreateChannel(name, flavorText); return await this.socketGuild.requestDoCreateChannel(name, flavorText);
} }
async requestDoRevokeToken(token: string): Promise<void> { async requestDoRevokeToken(token: string): Promise<void> {
await this.socketGuild.requestDoRevokeToken(token); await this.socketGuild.requestDoRevokeToken(token);
} }
async requestDoCreateToken(expiresAfter: string | null): Promise<Token> {
return await this.socketGuild.requestDoCreateToken(expiresAfter);
}
} }

View File

@ -69,8 +69,7 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
this.emit('update-resource', updatedResource); this.emit('update-resource', updatedResource);
}); });
// TODO: The server does not emit new-token this.socket.on('create-token', async (newDataToken: any) => {
this.socket.on('new-token', async (newDataToken: any) => {
const newToken = Token.fromDBData(newDataToken); const newToken = Token.fromDBData(newDataToken);
this.emit('new-tokens', [ newToken ]); this.emit('new-tokens', [ newToken ]);
}); });
@ -182,13 +181,19 @@ export default class SocketGuild extends EventEmitter<Connectable> implements As
async requestSetGuildIcon(guildIcon: Buffer): Promise<void> { async requestSetGuildIcon(guildIcon: Buffer): Promise<void> {
await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-icon', guildIcon); await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'set-icon', guildIcon);
} }
async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void> { async requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<Channel> {
const _changedChannel = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'update-channel', channelId, name, flavorText); const dataChangedChannel = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'update-channel', channelId, name, flavorText);
return Channel.fromDBData(dataChangedChannel);
} }
async requestDoCreateChannel(name: string, flavorText: string | null): Promise<void> { async requestDoCreateChannel(name: string, flavorText: string | null): Promise<Channel> {
const _newChannel = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'create-text-channel', name, flavorText); const dataNewChannel = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'create-text-channel', name, flavorText);
return Channel.fromDBData(dataNewChannel);
} }
async requestDoRevokeToken(token: string): Promise<void> { async requestDoRevokeToken(token: string): Promise<void> {
await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'revoke-token', token); await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'revoke-token', token);
} }
async requestDoCreateToken(expiresAfter: string | null): Promise<Token> {
const dataToken = await this.queryOnceVerified(Globals.DEFAULT_SOCKET_TIMEOUT, 'create-token', expiresAfter);
return Token.fromDBData(dataToken);
}
} }

View File

@ -49,9 +49,10 @@ export interface AsyncRequestable {
requestSetAvatar(avatar: Buffer): Promise<void>; requestSetAvatar(avatar: Buffer): Promise<void>;
requestSetGuildName(guildName: string): Promise<void>; requestSetGuildName(guildName: string): Promise<void>;
requestSetGuildIcon(guildIcon: Buffer): Promise<void>; requestSetGuildIcon(guildIcon: Buffer): Promise<void>;
requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<void>; requestDoUpdateChannel(channelId: string, name: string, flavorText: string | null): Promise<Channel>;
requestDoCreateChannel(name: string, flavorText: string | null): Promise<void>; requestDoCreateChannel(name: string, flavorText: string | null): Promise<Channel>;
requestDoRevokeToken(token: string): Promise<void>; requestDoRevokeToken(token: string): Promise<void>;
requestDoCreateToken(expiresAfter: string | null): Promise<Token>;
} }
export type Requestable = AsyncRequestable; export type Requestable = AsyncRequestable;

View File

@ -519,18 +519,18 @@ export default class DB {
static async getTokens(guildId: string): Promise<any[]> { static async getTokens(guildId: string): Promise<any[]> {
const result = await db.query(` const result = await db.query(`
SELECT "token", "member_id", "created", "expires" FROM tokens SELECT "id", "token", "member_id", "created", "expires" FROM tokens
WHERE "guild_id"=$1 WHERE "guild_id"=$1
`, [ guildId]); `, [ guildId]);
return result.rows; return result.rows;
} }
//insert into tokens (guild_id, expires) VALUES ('226b3e9e-5220-4205-bf5b-6738b9b3bb39', NOW() + '7 days'::interval) RETURNING "token", "expires"; //insert into tokens (guild_id, expires) VALUES ('226b3e9e-5220-4205-bf5b-6738b9b3bb39', NOW() + '7 days'::interval) RETURNING "token", "expires";
static async createToken(guildId: string, expiresAfter: string): Promise<any> { static async createToken(guildId: string, expiresAfter: string | null): Promise<any> {
const result = await db.query(` const result = await db.query(`
INSERT INTO "tokens" ("guild_id", "expires") INSERT INTO "tokens" ("guild_id", "expires")
VALUES ($1, NOW() + $2::interval) VALUES ($1, CASE WHEN $2::text IS NULL THEN NULL ELSE NOW() + $2::interval END)
RETURNING "token", "expires" RETURNING "id", "token", "member_id", "created", "expires"
`, [ guildId, expiresAfter ]); `, [ guildId, expiresAfter ]);
if (result.rows.length != 1) { if (result.rows.length != 1) {
throw new Error('unable to insert a token'); throw new Error('unable to insert a token');

File diff suppressed because it is too large Load Diff

View File

@ -77,7 +77,7 @@ $$ LANGUAGE plpgsql;
CREATE TABLE "tokens" ( CREATE TABLE "tokens" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4() "id" UUID NOT NULL DEFAULT uuid_generate_v4()
, "guild_id" UUID NOT NULL REFERENCES "guilds"("id") , "guild_id" UUID NOT NULL REFERENCES "guilds"("id")
, "expires" TIMESTAMP WITH TIME ZONE NOT NULL , "expires" TIMESTAMP WITH TIME ZONE -- Null means never expires
, "token" UUID NOT NULL DEFAULT uuid_generate_v4() , "token" UUID NOT NULL DEFAULT uuid_generate_v4()
, "member_id" UUID REFERENCES "members"("id") , "member_id" UUID REFERENCES "members"("id")
, "created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() , "created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()