Buttons can have hover text
This commit is contained in:
parent
c289f28bb3
commit
6c419440d5
2
makefile
2
makefile
@ -14,7 +14,7 @@ build-tsc:
|
||||
./node_modules/.bin/tsc -p ./src/tsconfig.json
|
||||
|
||||
build-sass:
|
||||
./node_modules/.bin/sass ./src/client/webapp/styles/styles.scss ./dist/client/webapp/styles/styles.css
|
||||
./node_modules/.bin/sass ./src/client/webapp/index.scss ./dist/client/webapp/index.css
|
||||
|
||||
build: lint build-tsc build-sass
|
||||
|
||||
|
@ -316,7 +316,7 @@ export class GuildMetadata implements WithEquals<GuildMetadata> {
|
||||
}
|
||||
}
|
||||
|
||||
export class GuildMetadataWithIds extends GuildMetadata {
|
||||
export class GuildMetadataLocal extends GuildMetadata {
|
||||
private constructor(
|
||||
public readonly id: number,
|
||||
name: string,
|
||||
@ -327,8 +327,8 @@ export class GuildMetadataWithIds extends GuildMetadata {
|
||||
super(name, iconResourceId, source);
|
||||
}
|
||||
|
||||
static fromDBData(data: any): GuildMetadataWithIds {
|
||||
return new GuildMetadataWithIds(
|
||||
static fromDBData(data: any): GuildMetadataLocal {
|
||||
return new GuildMetadataLocal(
|
||||
data.id,
|
||||
data.name,
|
||||
data.icon_resource_id,
|
||||
|
@ -9,4 +9,33 @@
|
||||
&.react:not(.aligned) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.basic-hover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: theme.$background-floating;
|
||||
|
||||
&.left {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.top {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.hover-text {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
color: theme.$interactive-normal;
|
||||
background-color: theme.$background-floating;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
import React, { FC, ReactNode, Ref, useCallback, useMemo } from 'react';
|
||||
import React, { FC, ReactNode, Ref, useCallback, useMemo, useRef } from 'react';
|
||||
import BasicHover, { BasicHoverSide } from '../contexts/context-hover-basic';
|
||||
import BaseElements from '../require/base-elements';
|
||||
import { useContextHover } from '../require/react-helper';
|
||||
|
||||
export enum ButtonColorType {
|
||||
BRAND = '',
|
||||
@ -12,17 +15,21 @@ interface ButtonProps {
|
||||
|
||||
colorType?: ButtonColorType;
|
||||
|
||||
hoverText?: string | null;
|
||||
|
||||
onClick?: () => void;
|
||||
shaking?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const DefaultButtonProps = {
|
||||
colorType: ButtonColorType.BRAND,
|
||||
}
|
||||
colorType: ButtonColorType.BRAND
|
||||
};
|
||||
|
||||
const Button: FC<ButtonProps> = React.forwardRef((props: ButtonProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { colorType, onClick, shaking, children } = { ...DefaultButtonProps, ...props };
|
||||
const Button: FC<ButtonProps> = (props: ButtonProps) => {
|
||||
const { colorType, hoverText, onClick, shaking, children } = { ...DefaultButtonProps, ...props };
|
||||
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const className = useMemo(
|
||||
() => [
|
||||
@ -38,7 +45,30 @@ const Button: FC<ButtonProps> = React.forwardRef((props: ButtonProps, ref: Ref<H
|
||||
if (onClick) onClick();
|
||||
}, [ shaking, onClick ]);
|
||||
|
||||
return <div ref={ref} className={className} onClick={clickHandler}>{children}</div>
|
||||
});
|
||||
const [ contextHover, mouseEnterCallable, mouseLeaveCallable ] = useContextHover(
|
||||
() => {
|
||||
if (!hoverText) return null;
|
||||
return (
|
||||
<BasicHover relativeToRef={buttonRef} realignDeps={[ hoverText ]} side={BasicHoverSide.BOTTOM}>
|
||||
{BaseElements.TAB_ABOVE}
|
||||
<div className="hover-text">{hoverText}</div>
|
||||
</BasicHover>
|
||||
);
|
||||
},
|
||||
[ hoverText ]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
ref={buttonRef} className={className}
|
||||
onClick={clickHandler}
|
||||
onMouseEnter={hoverText ? mouseEnterCallable : undefined}
|
||||
onMouseLeave={hoverText ? mouseLeaveCallable : undefined}
|
||||
>{children}</div>
|
||||
{contextHover}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
80
src/client/webapp/elements/components/token-row.tsx
Normal file
80
src/client/webapp/elements/components/token-row.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import moment from 'moment';
|
||||
import React, { FC } from 'react';
|
||||
import { GuildMetadata, Member, Token } from '../../data-types';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import Util from '../../util';
|
||||
import { IAddGuildData } from '../overlays/overlay-add-guild';
|
||||
import BaseElements from '../require/base-elements';
|
||||
import { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
|
||||
import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper';
|
||||
import Button, { ButtonColorType } from './button';
|
||||
|
||||
export interface TokenRowProps {
|
||||
url: string;
|
||||
guild: CombinedGuild;
|
||||
guildMeta: GuildMetadata;
|
||||
token: Token;
|
||||
}
|
||||
|
||||
const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
|
||||
const { guild, guildMeta, token } = props;
|
||||
|
||||
const [ guildSocketConfigs ] = useOneTimeAsyncAction(
|
||||
async () => await guild.fetchSocketConfigs(),
|
||||
null,
|
||||
[ guild ]
|
||||
);
|
||||
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guild, guildMeta.iconResourceId);
|
||||
|
||||
const [ revoke ] = useAsyncVoidCallback(async () => {
|
||||
await guild.requestDoRevokeToken(token.token);
|
||||
}, [ guild, token ]);
|
||||
|
||||
const [ downloadFunc, downloadText, downloadShaking ] = useDownloadButton(
|
||||
guildMeta.name + '.cordis',
|
||||
async () => {
|
||||
if (guildSocketConfigs === null) return null;
|
||||
const guildSocketConfig = Util.randomChoice(guildSocketConfigs);
|
||||
const addGuildData: IAddGuildData = {
|
||||
name: guildMeta.name,
|
||||
url: guildSocketConfig.url,
|
||||
cert: guildSocketConfig.cert,
|
||||
token: token.token,
|
||||
expires: token.expires?.getTime() ?? null,
|
||||
iconSrc: iconSrc,
|
||||
};
|
||||
const json = JSON.stringify(addGuildData);
|
||||
return Buffer.from(json);
|
||||
},
|
||||
[ guildSocketConfigs, guildMeta, token, iconSrc ]
|
||||
);
|
||||
|
||||
const userText = (token.member instanceof Member ? 'Used by ' + token.member.displayName : token.member?.id) ?? 'Unused Token';
|
||||
return (
|
||||
<div key={token.token} className="token-row">
|
||||
<div className="user-token">
|
||||
<div className="user">{userText}</div>
|
||||
<div className="token">{token.token}</div>
|
||||
</div>
|
||||
<div className="created-expires">
|
||||
<div className="created">{'Created ' + moment(token.created).fromNow()}</div>
|
||||
<div className="expires">{token.expires ? 'Expires ' + moment(token.expires).fromNow() : 'Never expires'}</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<Button
|
||||
colorType={ButtonColorType.BRAND}
|
||||
onClick={downloadFunc}
|
||||
hoverText={downloadText}
|
||||
shaking={downloadShaking}
|
||||
>{BaseElements.DOWNLOAD}</Button>
|
||||
<Button
|
||||
colorType={ButtonColorType.NEGATIVE}
|
||||
onClick={revoke}
|
||||
hoverText="Revoke"
|
||||
>{BaseElements.TRASHCAN}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TokenRow;
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, RefObject, useRef } from 'react'
|
||||
import React, { DependencyList, FC, ReactNode, RefObject, useRef } from 'react'
|
||||
import { IAlignment } from '../../require/elements-util';
|
||||
import { useCloseWhenEscapeOrClickedOrContextOutsideEffect } from '../../require/react-helper';
|
||||
import Context from './context';
|
||||
@ -7,20 +7,24 @@ export interface ContextMenuProps {
|
||||
relativeToRef?: RefObject<HTMLElement>;
|
||||
relativeToPos?: { x: number, y: number };
|
||||
alignment: IAlignment;
|
||||
realignDeps: DependencyList;
|
||||
children: ReactNode;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// Automatically closes when clicked outside of and includes a <div class="menu"> subelement
|
||||
const ContextMenu: FC<ContextMenuProps> = (props: ContextMenuProps) => {
|
||||
const { relativeToRef, relativeToPos, alignment, children, close } = props;
|
||||
const { relativeToRef, relativeToPos, alignment, realignDeps, children, close } = props;
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useCloseWhenEscapeOrClickedOrContextOutsideEffect(rootRef, close);
|
||||
|
||||
return (
|
||||
<Context rootRef={rootRef} relativeToRef={relativeToRef} relativeToPos={relativeToPos} alignment={alignment}>
|
||||
<Context
|
||||
rootRef={rootRef} relativeToRef={relativeToRef} relativeToPos={relativeToPos}
|
||||
alignment={alignment} realignDeps={realignDeps}
|
||||
>
|
||||
<div className="menu">
|
||||
{children}
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, RefObject, useRef } from 'react';
|
||||
import React, { DependencyList, FC, ReactNode, RefObject, useRef } from 'react';
|
||||
import { IAlignment } from '../../require/elements-util';
|
||||
import { useAlignment } from '../../require/react-helper';
|
||||
|
||||
@ -7,17 +7,23 @@ export interface ContextProps {
|
||||
relativeToRef?: RefObject<HTMLElement>;
|
||||
relativeToPos?: { x: number, y: number };
|
||||
alignment: IAlignment;
|
||||
realignDeps: DependencyList;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// You should create a component like context-menu.tsx instead of using this class directly.
|
||||
const Context: FC<ContextProps> = (props: ContextProps) => {
|
||||
const { rootRef, relativeToRef, relativeToPos, alignment, children } = props;
|
||||
const { rootRef, relativeToRef, relativeToPos, alignment, realignDeps, children } = props;
|
||||
|
||||
const myRootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [ className ] = useAlignment(
|
||||
rootRef ?? myRootRef, relativeToRef ?? null, relativeToPos ?? null, alignment, 'context react'
|
||||
rootRef ?? myRootRef,
|
||||
relativeToRef ?? null,
|
||||
relativeToPos ?? null,
|
||||
alignment,
|
||||
realignDeps,
|
||||
'context react'
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, ReactNode, RefObject, useMemo } from 'react';
|
||||
import React, { DependencyList, FC, ReactNode, RefObject, useMemo } from 'react';
|
||||
import { ShouldNeverHappenError } from '../../data-types';
|
||||
import Context from './components/context';
|
||||
|
||||
@ -12,11 +12,12 @@ export enum BasicHoverSide {
|
||||
export interface BasicHoverProps {
|
||||
side: BasicHoverSide;
|
||||
relativeToRef: RefObject<HTMLElement>;
|
||||
realignDeps: DependencyList;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const BasicHover: FC<BasicHoverProps> = (props: BasicHoverProps) => {
|
||||
const { side, relativeToRef, children } = props;
|
||||
const { side, relativeToRef, realignDeps, children } = props;
|
||||
|
||||
const alignment = useMemo(() => {
|
||||
switch (side) {
|
||||
@ -34,7 +35,7 @@ const BasicHover: FC<BasicHoverProps> = (props: BasicHoverProps) => {
|
||||
}, [ side ]);
|
||||
|
||||
return (
|
||||
<Context alignment={alignment} relativeToRef={relativeToRef}>
|
||||
<Context alignment={alignment} relativeToRef={relativeToRef} realignDeps={realignDeps}>
|
||||
<div className={'basic-hover ' + side}>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@ const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: Co
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContextMenu relativeToRef={relativeToRef} alignment={alignment} close={close}>
|
||||
<ContextMenu relativeToRef={relativeToRef} alignment={alignment} realignDeps={[]} close={close}>
|
||||
<div className="member-context">
|
||||
<div onClick={openPersonalize} className="item">
|
||||
<div className="icon"><img src="./img/pencil-icon.png" /></div>
|
||||
|
@ -8,7 +8,7 @@ import ContextMenu from './components/context-menu';
|
||||
|
||||
export interface GuildTitleContextMenuProps {
|
||||
close: () => void;
|
||||
relativeToRef: RefObject<HTMLElement>
|
||||
relativeToRef: RefObject<HTMLElement>;
|
||||
guild: CombinedGuild;
|
||||
guildMeta: GuildMetadata;
|
||||
selfMember: Member;
|
||||
@ -55,7 +55,7 @@ const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitle
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContextMenu alignment={alignment} close={close} relativeToRef={relativeToRef}>
|
||||
<ContextMenu alignment={alignment} close={close} relativeToRef={relativeToRef} realignDeps={[ guild, guildMeta, selfMember ]}>
|
||||
<div className="guild-title-context-menu">
|
||||
{guildSettingsElement}
|
||||
{createChannelElement}
|
||||
|
@ -51,7 +51,8 @@ const ImageContextMenu: FC<ImageContextMenuProps> = (props: ImageContextMenuProp
|
||||
);
|
||||
|
||||
const [ saveCallable, saveText, saveShaking ] = useDownloadButton(
|
||||
resourceName + (isPreview ? '-preview.jpg' : ''), resourceBuff,
|
||||
resourceName + (isPreview ? '-preview.jpg' : ''),
|
||||
async () => resourceBuff, [ resourceBuff ],
|
||||
{
|
||||
start: 'Save Image' + previewText,
|
||||
pendingSave: 'Saving' + previewText + '...',
|
||||
@ -62,7 +63,7 @@ const ImageContextMenu: FC<ImageContextMenuProps> = (props: ImageContextMenuProp
|
||||
const alignment = useMemo(() => ({ top: 'centerY', left: 'centerX' }), []);
|
||||
|
||||
return (
|
||||
<ContextMenu alignment={alignment} relativeToPos={relativeToPos} close={close}>
|
||||
<ContextMenu alignment={alignment} relativeToPos={relativeToPos} realignDeps={[ copyText, saveText ]} close={close}>
|
||||
<div className="image">
|
||||
<div className="item copy-image" onClick={copyCallable}>{copyText}</div>
|
||||
<div className="item save-image" onClick={saveCallable}>{saveText}</div>
|
||||
|
@ -7,14 +7,14 @@ import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import Display from '../components/display';
|
||||
import InvitePreview from '../components/invite-preview';
|
||||
import { Member, Token } from '../../data-types';
|
||||
import { Token } from '../../data-types';
|
||||
import { useAsyncSubmitButton } from '../require/react-helper';
|
||||
import { Duration } from 'moment';
|
||||
import moment from 'moment';
|
||||
import DropdownInput from '../components/input-dropdown';
|
||||
import Button, { ButtonColorType } from '../components/button';
|
||||
import BaseElements from '../require/base-elements';
|
||||
import Button from '../components/button';
|
||||
import { useTokensSubscription, useGuildMetadataSubscription, useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
|
||||
import TokenRow from '../components/token-row';
|
||||
|
||||
|
||||
export interface GuildInvitesDisplayProps {
|
||||
@ -69,29 +69,11 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
// TODO: Try Again
|
||||
return <div className="tokens-failed">Unable to load tokens</div>;
|
||||
}
|
||||
return tokens?.map((token: Token) => {
|
||||
const userText = (token.member instanceof Member ? 'Used by ' + token.member.displayName : token.member?.id) ?? 'Unused Token';
|
||||
return (
|
||||
<div key={token.token} className="token-row">
|
||||
<div className="user-token">
|
||||
<div className="user">{userText}</div>
|
||||
<div className="token">{token.token}</div>
|
||||
</div>
|
||||
<div className="created-expires">
|
||||
<div className="created">{'Created ' + moment(token.created).fromNow()}</div>
|
||||
<div className="expires">{token.expires ? 'Expires ' + moment(token.expires).fromNow() : 'Never expires'}</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<Button colorType={ButtonColorType.BRAND}>{BaseElements.DOWNLOAD}</Button>
|
||||
<Button
|
||||
colorType={ButtonColorType.NEGATIVE}
|
||||
onClick={async () => await guild.requestDoRevokeToken(token.token)}
|
||||
>{BaseElements.TRASHCAN}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}, [ tokens, tokensError ]);
|
||||
if (!guildMeta) {
|
||||
return <div className="no-guild-meta">No Guild Metadata</div>;
|
||||
}
|
||||
return tokens?.map((token: Token) => <TokenRow key={token.token} url={url} guild={guild} token={token} guildMeta={guildMeta} />);
|
||||
}, [ url, guild, tokens, tokensError ]);
|
||||
|
||||
return (
|
||||
<Display
|
||||
|
@ -30,10 +30,10 @@ const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) =>
|
||||
const [ modifyContextHover, modifyMouseEnterCallable, modifyMouseLeaveCallable ] = useContextHover(
|
||||
() => {
|
||||
return (
|
||||
<BasicHover side={BasicHoverSide.RIGHT} relativeToRef={modifyRef}>
|
||||
<BasicHover side={BasicHoverSide.RIGHT} relativeToRef={modifyRef} realignDeps={[]}>
|
||||
<div className="channel-gear-hover">
|
||||
<div className="tab">{BaseElements.TAB_LEFT}</div>
|
||||
<div className="text">Modify Channel</div>
|
||||
<div className="hover-text">Modify Channel</div>
|
||||
</div>
|
||||
</BasicHover>
|
||||
);
|
||||
@ -55,7 +55,8 @@ const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) =>
|
||||
<div className="name">{channel.name}</div>
|
||||
<div
|
||||
className="modify" ref={modifyRef} onClick={launchModify}
|
||||
onMouseEnter={modifyMouseEnterCallable} onMouseLeave={modifyMouseLeaveCallable}
|
||||
onMouseEnter={modifyMouseEnterCallable}
|
||||
onMouseLeave={modifyMouseLeaveCallable}
|
||||
>{BaseElements.COG}</div>
|
||||
{modifyContextHover}
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
|
||||
if (!selfMember) return null;
|
||||
const nameStyle = selfMember.roleColor ? { color: selfMember.roleColor } : {};
|
||||
return (
|
||||
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT}>
|
||||
<BasicHover relativeToRef={rootRef} side={BasicHoverSide.RIGHT} realignDeps={[ guildMeta, selfMember ]}>
|
||||
<div className="guild-hover">
|
||||
<div className="tab">{BaseElements.TAB_LEFT}</div>
|
||||
<div className="info">
|
||||
@ -54,7 +54,7 @@ const GuildListElement: FC<GuildListElementProps> = (props: GuildListElementProp
|
||||
const [ contextClickMenu, onContextClick ] = useContextClickContextMenu((alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => {
|
||||
return (
|
||||
<ContextMenu
|
||||
alignment={alignment} relativeToPos={relativeToPos} close={close}
|
||||
alignment={alignment} relativeToPos={relativeToPos} realignDeps={[]} close={close}
|
||||
>
|
||||
<div className="guild-context">
|
||||
<div className="item red leave-guild" onClick={leaveGuildCallable}>Leave Guild</div>
|
||||
|
@ -19,7 +19,7 @@ const ResourceElement: FC<ResourceElementProps> = (props: ResourceElementProps)
|
||||
|
||||
const [ callable, text, shaking ] = useDownloadButton(
|
||||
resourceName,
|
||||
{ guild, resourceId },
|
||||
async () => (await guild.fetchResource(resourceId)).data, [ guild, resourceId ],
|
||||
{
|
||||
start: 'Click to Download',
|
||||
pendingFetch: 'Fetching...', errorFetch: 'Unable to Download. Try Again',
|
||||
|
@ -23,7 +23,7 @@ export interface IAddGuildData {
|
||||
url: string,
|
||||
cert: string,
|
||||
token: string,
|
||||
expires: number,
|
||||
expires: number | null,
|
||||
iconSrc: string
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const expired = addGuildData.expires < new Date().getTime();
|
||||
const expired = addGuildData.expires && addGuildData.expires < new Date().getTime();
|
||||
const exampleDisplayName = useMemo(() => getExampleDisplayName(), []);
|
||||
const exampleAvatarPath = useMemo(() => getExampleAvatarPath(), []);
|
||||
|
||||
@ -125,7 +125,7 @@ const AddGuildOverlay: FC<AddGuildOverlayProps> = (props: AddGuildOverlayProps)
|
||||
<div ref={rootRef} className="content add-guild">
|
||||
<InvitePreview
|
||||
name={addGuildData.name} iconSrc={addGuildData.iconSrc}
|
||||
url={addGuildData.url} expiresFromNow={moment.duration(addGuildData.expires - Date.now(), 'ms')}
|
||||
url={addGuildData.url} expiresFromNow={addGuildData.expires ? moment.duration(addGuildData.expires - Date.now(), 'ms') : null}
|
||||
/>
|
||||
<div className="divider"></div>
|
||||
<div className="personalization">
|
||||
|
@ -17,7 +17,7 @@ const ErrorMessageOverlay: FC<ErrorMessageOverlayProps> = (props: ErrorMessageOv
|
||||
<div className="icon">
|
||||
<img src="./img/error.png" alt="error" />
|
||||
</div>
|
||||
<div className="text">
|
||||
<div className="hover-text">
|
||||
<div className="title">{title}</div>
|
||||
<div className="message">{message}</div>
|
||||
</div>
|
||||
|
@ -192,6 +192,11 @@ export default class BaseElements {
|
||||
L 0,8
|
||||
Z` }
|
||||
};
|
||||
static TAB_ABOVE = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10" viewBox="0 0 12 8">
|
||||
<path fill="currentColor" d="M 6,0 L 12,8 L 0,8 Z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
static Q_TAB_BELOW = {
|
||||
ns: 'http://www.w3.org/2000/svg', tag: 'svg', width: 12, height: 8, viewBox: '0 0 12 8', content:
|
||||
|
@ -6,7 +6,6 @@ const LOG = Logger.create(__filename, electronConsole);
|
||||
import { DependencyList, Dispatch, MutableRefObject, ReactNode, RefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ShouldNeverHappenError } from "../../data-types";
|
||||
import Util from '../../util';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import Globals from '../../globals';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
@ -124,17 +123,22 @@ export function useAsyncVoidCallback(
|
||||
return [ callable ];
|
||||
}
|
||||
|
||||
// TODO: If this function gets expanded one more time, downloadSrc needs to be changed to fetchBuff and only
|
||||
// be a function. Having 3 types for downloadSrc is a bit iffy.
|
||||
export function useDownloadButton(
|
||||
downloadName: string,
|
||||
downloadSrc: { guild: CombinedGuild, resourceId: string } | Buffer,
|
||||
stateTextMapping?: { start?: string, pendingFetch?: string, errorFetch?: string, pendingSave?: string, errorSave?: string, success?: string }
|
||||
): [ callable: () => void, text: string, shaking: boolean ] {
|
||||
const textMapping = { ...{ start: 'Download', pendingFetch: 'Downloading...', errorFetch: 'Try Again', pendingSave: 'Saving...', errorSave: 'Try Again', success: 'Open in Explorer' }, ...stateTextMapping };
|
||||
|
||||
const downloadBuff = downloadSrc instanceof Buffer ? downloadSrc : null;
|
||||
fetchBuff: () => Promise<Buffer | null>,
|
||||
fetchBuffDeps: DependencyList,
|
||||
stateTextMapping?: { start?: string, pendingFetch?: string, errorFetch?: string, notReadyFetch?: string, pendingSave?: string, errorSave?: string, success?: string }
|
||||
): [
|
||||
callable: () => void,
|
||||
text: string,
|
||||
shaking: boolean
|
||||
] {
|
||||
const textMapping = { ...{ start: 'Download', pendingFetch: 'Downloading...', errorFetch: 'Try Again', notReadyFetch: 'Not Ready', pendingSave: 'Saving...', errorSave: 'Try Again', success: 'Open in Explorer' }, ...stateTextMapping };
|
||||
|
||||
const [ filePath, setFilePath ] = useState<string | null>(null);
|
||||
const [ fileBuffer, setFileBuffer ] = useState<Buffer | null>(downloadBuff);
|
||||
const [ fileBuffer, setFileBuffer ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ text, setText ] = useState<string>(textMapping.start);
|
||||
const [ shaking, doShake ] = useShake(400);
|
||||
@ -159,25 +163,20 @@ export function useDownloadButton(
|
||||
|
||||
let saveBuffer = fileBuffer;
|
||||
if (saveBuffer === null) {
|
||||
if (!(downloadSrc instanceof Buffer)) {
|
||||
// fetch the buffer
|
||||
const { guild, resourceId } = downloadSrc;
|
||||
try {
|
||||
setText(textMapping.pendingFetch);
|
||||
const resource = await guild.fetchResource(resourceId);
|
||||
if (!isMounted.current) return;
|
||||
setFileBuffer(resource.data);
|
||||
saveBuffer = resource.data;
|
||||
} catch (e: unknown) {
|
||||
LOG.error('Error fetching resource for download button', e);
|
||||
if (!isMounted.current) return;
|
||||
setText(textMapping.errorFetch);
|
||||
try {
|
||||
setText(textMapping.pendingFetch);
|
||||
const data = await fetchBuff();
|
||||
if (!isMounted.current) return;
|
||||
if (data === null) {
|
||||
setText(textMapping.notReadyFetch);
|
||||
doShake();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Save buffer not specified
|
||||
LOG.error('Bad download setup');
|
||||
setFileBuffer(data);
|
||||
saveBuffer = data;
|
||||
} catch (e: unknown) {
|
||||
LOG.error('Error executing download function for download button', e);
|
||||
if (!isMounted.current) return;
|
||||
setText(textMapping.errorFetch);
|
||||
doShake();
|
||||
return;
|
||||
@ -204,7 +203,7 @@ export function useDownloadButton(
|
||||
|
||||
setText(textMapping.success);
|
||||
},
|
||||
[ downloadName, downloadSrc, filePath, fileBuffer ]
|
||||
[ downloadName, fetchBuff, ...fetchBuffDeps, filePath, fileBuffer ]
|
||||
);
|
||||
|
||||
return [ callable, text, shaking ];
|
||||
@ -420,6 +419,7 @@ export function useAlignment(
|
||||
relativeToRef: RefObject<HTMLElement | null> | null,
|
||||
relativeToPos: { x: number, y: number } | null,
|
||||
alignment: IAlignment,
|
||||
realignDeps: DependencyList,
|
||||
baseClassName: string
|
||||
): [
|
||||
className: string
|
||||
@ -432,7 +432,7 @@ export function useAlignment(
|
||||
if (!relativeTo) throw new ShouldNeverHappenError('invalid alignment props');
|
||||
ElementsUtil.alignContextElement(rootRef.current, relativeTo, alignment);
|
||||
setAligned(true);
|
||||
}, [ rootRef, relativeToRef, relativeToPos ]);
|
||||
}, [ rootRef, relativeToRef, relativeToPos, ...realignDeps ]);
|
||||
|
||||
const className = useMemo(() => {
|
||||
return baseClassName + (aligned ? ' aligned' : '');
|
||||
|
@ -29,7 +29,7 @@ const GuildListContainer: FC<GuildListContainerProps> = (props: GuildListContain
|
||||
|
||||
const [ contextHover, onMouseEnter, onMouseLeave ] = useContextHover(() => {
|
||||
return (
|
||||
<BasicHover relativeToRef={addGuildRef} side={BasicHoverSide.RIGHT}>
|
||||
<BasicHover relativeToRef={addGuildRef} realignDeps={[]} side={BasicHoverSide.RIGHT}>
|
||||
<div className="add-guild-hover">
|
||||
<div className="tab">{BaseElements.TAB_LEFT}</div>
|
||||
<div className="info">Add Guild</div>
|
||||
|
@ -7,7 +7,7 @@ import * as socketio from 'socket.io-client';
|
||||
import PersonalDBGuild from './guild-personal-db';
|
||||
import RAMGuild from './guild-ram';
|
||||
import SocketGuild from './guild-socket';
|
||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataLocal, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
|
||||
import MessageRAMCache from "./message-ram-cache";
|
||||
import PersonalDB from "./personal-db";
|
||||
@ -195,7 +195,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
}
|
||||
|
||||
static async create(
|
||||
guildMetadata: GuildMetadataWithIds,
|
||||
guildMetadata: GuildMetadataLocal,
|
||||
socketConfig: SocketConfig,
|
||||
messageRAMCache: MessageRAMCache,
|
||||
resourceRAMCache: ResourceRAMCache,
|
||||
@ -268,6 +268,10 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
||||
return this.ramGuild.getChannels();
|
||||
}
|
||||
|
||||
async fetchSocketConfigs(): Promise<SocketConfig[]> {
|
||||
return await this.personalDBGuild.fetchSocketConfigs();
|
||||
}
|
||||
|
||||
async fetchConnectionInfo(): Promise<ConnectionInfo> {
|
||||
const connection: ConnectionInfo = {
|
||||
id: null,
|
||||
|
@ -4,7 +4,7 @@ import Logger from '../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import { AsyncFetchable, AsyncLackable } from "./guild-types";
|
||||
import { Channel, GuildMetadata, Member, Message, Resource, Token } from "./data-types";
|
||||
import { Channel, GuildMetadata, GuildMetadataLocal, Member, Message, Resource, SocketConfig, Token } from "./data-types";
|
||||
|
||||
import PersonalDB from "./personal-db";
|
||||
|
||||
@ -17,7 +17,11 @@ export default class PersonalDBGuild implements AsyncFetchable, AsyncLackable {
|
||||
|
||||
// Fetched Methods
|
||||
|
||||
async fetchMetadata(): Promise<GuildMetadata | null> {
|
||||
async fetchSocketConfigs(): Promise<SocketConfig[]> {
|
||||
return await this.db.fetchGuildSockets(this.guildId);
|
||||
}
|
||||
|
||||
async fetchMetadata(): Promise<GuildMetadataLocal | null> {
|
||||
return this.db.fetchGuild(this.guildId);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import * as socketio from 'socket.io-client';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { Changes, Channel, GuildMetadata, GuildMetadataWithIds, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
import { Changes, Channel, GuildMetadata, GuildMetadataLocal, Member, Message, Resource, SocketConfig, Token } from './data-types';
|
||||
import { IAddGuildData } from './elements/overlays/overlay-add-guild';
|
||||
import { EventEmitter } from 'tsee';
|
||||
import CombinedGuild from './guild-combined';
|
||||
@ -65,7 +65,7 @@ export default class GuildsManager extends EventEmitter<{
|
||||
super();
|
||||
}
|
||||
|
||||
async _connectFromConfig(guildMetadata: GuildMetadataWithIds, socketConfig: SocketConfig): Promise<CombinedGuild> {
|
||||
async _connectFromConfig(guildMetadata: GuildMetadataLocal, socketConfig: SocketConfig): Promise<CombinedGuild> {
|
||||
LOG.debug(`connecting to g#${guildMetadata.id} at ${socketConfig.url}`);
|
||||
|
||||
const guild = await CombinedGuild.create(
|
||||
@ -166,7 +166,7 @@ export default class GuildsManager extends EventEmitter<{
|
||||
try {
|
||||
const member = Member.fromDBData(dataMember);
|
||||
const meta = GuildMetadata.fromDBData(dataMetadata);
|
||||
let guildMeta: GuildMetadataWithIds | null = null;
|
||||
let guildMeta: GuildMetadataLocal | null = null;
|
||||
let socketConfig: SocketConfig | null = null;
|
||||
await this.personalDB.queueTransaction(async () => {
|
||||
const guildId = await this.personalDB.addGuild(meta.name, meta.iconResourceId, member.id);
|
||||
|
@ -10,7 +10,7 @@ import ConcurrentQueue from "../../concurrent-queue/concurrent-queue";
|
||||
import * as sqlite from 'sqlite';
|
||||
import * as sqlite3 from 'sqlite3';
|
||||
|
||||
import { Channel, GuildMetadataWithIds, Member, Message, Resource, SocketConfig } from "./data-types";
|
||||
import { Channel, GuildMetadataLocal, Member, Message, Resource, SocketConfig } from "./data-types";
|
||||
|
||||
export default class PersonalDB {
|
||||
private transactions = new ConcurrentQueue(1);
|
||||
@ -192,18 +192,18 @@ export default class PersonalDB {
|
||||
if (result?.changes !== 1) throw new Error('unable to update guild icon');
|
||||
}
|
||||
|
||||
async fetchGuild(guildId: number): Promise<GuildMetadataWithIds> {
|
||||
async fetchGuild(guildId: number): Promise<GuildMetadataLocal> {
|
||||
const result = await this.db.get(
|
||||
`SELECT * FROM guilds WHERE id=:id`,
|
||||
{ ':id': guildId }
|
||||
);
|
||||
if (!result) throw new Error('unable to fetch guild');
|
||||
return GuildMetadataWithIds.fromDBData(result);
|
||||
return GuildMetadataLocal.fromDBData(result);
|
||||
}
|
||||
|
||||
async fetchGuilds(): Promise<GuildMetadataWithIds[]> {
|
||||
async fetchGuilds(): Promise<GuildMetadataLocal[]> {
|
||||
const result = await this.db.all('SELECT * FROM guilds');
|
||||
return result.map(dataGuild => GuildMetadataWithIds.fromDBData(dataGuild));
|
||||
return result.map(dataGuild => GuildMetadataLocal.fromDBData(dataGuild));
|
||||
}
|
||||
|
||||
// Guild Sockets
|
||||
|
@ -104,4 +104,8 @@ export default class Util {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static randomChoice<T>(list: T[]): T {
|
||||
return list[Math.floor(Math.random() * list.length)] as T;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user