create invite token dialog
This commit is contained in:
parent
d875a8f8fb
commit
84d0a0e542
@ -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}
|
||||||
|
@ -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');
|
||||||
|
@ -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 ];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user