styled error messages. bug where clicking to fetch above (and I assume below too) doesn't actually run anything
This commit is contained in:
parent
73e81f0bc9
commit
aa79034d7e
@ -12,7 +12,9 @@ $borderRadius: 8px;
|
|||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
padding-bottom: calc(16px + 8px);
|
padding-bottom: calc(16px + 8px);
|
||||||
margin-bottom: calc(65px - $scrollbarBottom - $borderRadius);
|
margin-bottom: calc(65px - $scrollbarBottom - $borderRadius);
|
||||||
height: calc(100vh - 71px - 65px + 4px + 8px); /* TODO: Going to have to find a way to do this without a fixed height since the message box needs to be scalable */
|
height: calc(
|
||||||
|
100vh - 71px - 65px + 4px + 8px
|
||||||
|
); /* TODO: Going to have to find a way to do this without a fixed height since the message box needs to be scalable */
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
@ -44,6 +46,32 @@ $borderRadius: 8px;
|
|||||||
background-color: theme.$background-message-hover;
|
background-color: theme.$background-message-hover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: theme.$background-secondary-alt;
|
||||||
|
margin-left: 16px;
|
||||||
|
margin-right: 16px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
margin-top: 8px;
|
||||||
|
&:first-child {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: theme.$text-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { MutableRefObject, ReactNode, RefObject, useCallback, useRef } from 'react';
|
import React, { MutableRefObject, ReactNode } from 'react';
|
||||||
|
|
||||||
import { LoadableValueScrolling } from '../require/loadables';
|
import { LoadableValueScrolling } from '../require/loadables';
|
||||||
import { useScrollableCallables } from '../require/react-helper';
|
import { useScrollableCallables } from '../require/react-helper';
|
||||||
@ -23,6 +23,7 @@ function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
|
|||||||
<div className="infinite-scroll-elements">
|
<div className="infinite-scroll-elements">
|
||||||
<Retry error={scrollable?.above?.error} text={aboveErrorMessage} retryFunc={fetchAboveCallable} />
|
<Retry error={scrollable?.above?.error} text={aboveErrorMessage} retryFunc={fetchAboveCallable} />
|
||||||
{children}
|
{children}
|
||||||
|
<Retry error={scrollable?.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
|
||||||
<Retry
|
<Retry
|
||||||
error={scrollable.error}
|
error={scrollable.error}
|
||||||
text={initialErrorMessage}
|
text={initialErrorMessage}
|
||||||
@ -33,7 +34,6 @@ function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Retry error={scrollable?.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -23,9 +23,11 @@ const Retry: FC<RetryProps> = (props: RetryProps) => {
|
|||||||
return { result: null, errorMessage: null };
|
return { result: null, errorMessage: null };
|
||||||
},
|
},
|
||||||
[retryFunc],
|
[retryFunc],
|
||||||
{ start: 'Try Again', pending: 'Fetching...' },
|
{ start: 'Try Again', pending: 'Fetching...', done: 'Try Again', error: 'Try Again' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: shaking isn't working
|
||||||
|
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,7 +39,11 @@ export type AtomEffectParam<T> = Arguments<AtomEffect<T>>[0];
|
|||||||
|
|
||||||
// "initial" value loaders
|
// "initial" value loaders
|
||||||
export type FetchValueFunc = () => Promise<void>;
|
export type FetchValueFunc = () => Promise<void>;
|
||||||
export function createFetchValueFunc<T>(atomEffectParam: AtomEffectParam<LoadableValue<T>>, guildId: number, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>): FetchValueFunc {
|
export function createFetchValueFunc<T>(
|
||||||
|
atomEffectParam: AtomEffectParam<LoadableValue<T>>,
|
||||||
|
guildId: number,
|
||||||
|
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
|
||||||
|
): FetchValueFunc {
|
||||||
const { node, setSelf, getPromise } = atomEffectParam;
|
const { node, setSelf, getPromise } = atomEffectParam;
|
||||||
const fetchValueFunc = async () => {
|
const fetchValueFunc = async () => {
|
||||||
const guild = await getPromise(guildState(guildId));
|
const guild = await getPromise(guildState(guildId));
|
||||||
@ -53,8 +57,8 @@ export function createFetchValueFunc<T>(atomEffectParam: AtomEffectParam<Loadabl
|
|||||||
const value = await fetchFunc(guild);
|
const value = await fetchFunc(guild);
|
||||||
setSelf(createLoadedValue(value, fetchValueFunc));
|
setSelf(createLoadedValue(value, fetchValueFunc));
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
LOG.error('unable to fetch initial guild metadata', e);
|
|
||||||
setSelf(createFailedValue(e, fetchValueFunc));
|
setSelf(createFailedValue(e, fetchValueFunc));
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return fetchValueFunc;
|
return fetchValueFunc;
|
||||||
@ -84,10 +88,17 @@ export function createFetchValueScrollingFunc<T, E>(
|
|||||||
setSelf(DEF_PENDED_SCROLLING_VALUE);
|
setSelf(DEF_PENDED_SCROLLING_VALUE);
|
||||||
try {
|
try {
|
||||||
const result = await fetchFunc(guild, count);
|
const result = await fetchFunc(guild, count);
|
||||||
setSelf(createLoadedValueScrolling(result, fetchValueReferenceFunc, createAboveEndFunc(result), createBelowEndFunc(result)));
|
setSelf(
|
||||||
|
createLoadedValueScrolling(
|
||||||
|
result,
|
||||||
|
fetchValueReferenceFunc,
|
||||||
|
createAboveEndFunc(result),
|
||||||
|
createBelowEndFunc(result),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
LOG.error('unable to fetch value scrolling', e);
|
|
||||||
setSelf(createFailedValueScrolling(e, fetchValueReferenceFunc));
|
setSelf(createFailedValueScrolling(e, fetchValueReferenceFunc));
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return fetchValueReferenceFunc;
|
return fetchValueReferenceFunc;
|
||||||
@ -98,8 +109,15 @@ export function createFetchValueScrollingReferenceFunc<T>(
|
|||||||
atomEffectParam: AtomEffectParam<LoadableValueScrolling<T[], T>>,
|
atomEffectParam: AtomEffectParam<LoadableValueScrolling<T[], T>>,
|
||||||
guildId: number,
|
guildId: number,
|
||||||
getFunc: (selfState: LoadedValueScrolling<T[], T>) => LoadableScrollingEnd<T>,
|
getFunc: (selfState: LoadedValueScrolling<T[], T>) => LoadableScrollingEnd<T>,
|
||||||
applyEndToSelf: (selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => LoadedValueScrolling<T[], T>,
|
applyEndToSelf: (
|
||||||
applyResultToSelf: (selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => LoadedValueScrolling<T[], T>,
|
selfState: LoadedValueScrolling<T[], T>,
|
||||||
|
end: LoadableScrollingEnd<T>,
|
||||||
|
) => LoadedValueScrolling<T[], T>,
|
||||||
|
applyResultToSelf: (
|
||||||
|
selfState: LoadedValueScrolling<T[], T>,
|
||||||
|
end: LoadedScrollingEnd<T>,
|
||||||
|
result: T[],
|
||||||
|
) => LoadedValueScrolling<T[], T>,
|
||||||
count: number,
|
count: number,
|
||||||
fetchReferenceFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>,
|
fetchReferenceFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>,
|
||||||
): {
|
): {
|
||||||
@ -134,15 +152,22 @@ export function createFetchValueScrollingReferenceFunc<T>(
|
|||||||
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
|
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
|
||||||
} else {
|
} else {
|
||||||
const hasMore = result.length >= count;
|
const hasMore = result.length >= count;
|
||||||
setSelf(applyResultToSelf(selfState, createLoadedScrollingEnd(hasMore, fetchValueReferenceFunc, cancel), result));
|
setSelf(
|
||||||
|
applyResultToSelf(
|
||||||
|
selfState,
|
||||||
|
createLoadedScrollingEnd(hasMore, fetchValueReferenceFunc, cancel),
|
||||||
|
result,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
LOG.error('unable to fetch value based on reference (but we were canceled)', e);
|
LOG.error('unable to fetch value based on reference (but we were canceled anyway)', e);
|
||||||
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
|
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
|
||||||
} else {
|
} else {
|
||||||
LOG.error('unable to fetch value based on reference', e);
|
// xLOG.error('unable to fetch value based on reference', e);
|
||||||
setSelf(applyEndToSelf(selfState, createFailedScrollingEnd(e, fetchValueReferenceFunc, cancel)));
|
setSelf(applyEndToSelf(selfState, createFailedScrollingEnd(e, fetchValueReferenceFunc, cancel)));
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -156,7 +181,11 @@ function createEventHandler<
|
|||||||
ArgsMapResult, // e.g. Member[]
|
ArgsMapResult, // e.g. Member[]
|
||||||
// eslint-disable-next-line space-before-function-paren
|
// eslint-disable-next-line space-before-function-paren
|
||||||
XE extends keyof (Connectable | Conflictable), // e.g. new-members
|
XE extends keyof (Connectable | Conflictable), // e.g. new-members
|
||||||
>(atomEffectParam: AtomEffectParam<V>, argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult, applyFunc: (selfState: V, argsResult: ArgsMapResult) => V): (Connectable & Conflictable)[XE] {
|
>(
|
||||||
|
atomEffectParam: AtomEffectParam<V>,
|
||||||
|
argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult,
|
||||||
|
applyFunc: (selfState: V, argsResult: ArgsMapResult) => V,
|
||||||
|
): (Connectable & Conflictable)[XE] {
|
||||||
const { node, setSelf, getPromise } = atomEffectParam;
|
const { node, setSelf, getPromise } = atomEffectParam;
|
||||||
// i think the typed EventEmitter class isn't ready for this level of insane type safety
|
// i think the typed EventEmitter class isn't ready for this level of insane type safety
|
||||||
// otherwise, I may have done this wrong. Forcing it to work with these calls
|
// otherwise, I may have done this wrong. Forcing it to work with these calls
|
||||||
@ -198,8 +227,16 @@ export function listenToSingle<
|
|||||||
if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager
|
if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager
|
||||||
if (closed) return; // make sure not to bind events if this closed while we were fetching the guild state
|
if (closed) return; // make sure not to bind events if this closed while we were fetching the guild state
|
||||||
|
|
||||||
onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc);
|
onUpdateFunc = createEventHandler(
|
||||||
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc);
|
atomEffectParam,
|
||||||
|
eventMapping.updatedEvent.argsMap,
|
||||||
|
eventMapping.updatedEvent.applyFunc,
|
||||||
|
);
|
||||||
|
onConflictFunc = createEventHandler(
|
||||||
|
atomEffectParam,
|
||||||
|
eventMapping.conflictEvent.argsMap,
|
||||||
|
eventMapping.conflictEvent.applyFunc,
|
||||||
|
);
|
||||||
|
|
||||||
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
|
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
|
||||||
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
|
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
|
||||||
@ -212,7 +249,14 @@ export function listenToSingle<
|
|||||||
return cleanup;
|
return cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleEventMappingParams<T, V, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable> {
|
export interface MultipleEventMappingParams<
|
||||||
|
T,
|
||||||
|
V,
|
||||||
|
NE extends keyof Connectable,
|
||||||
|
UE extends keyof Connectable,
|
||||||
|
RE extends keyof Connectable,
|
||||||
|
CE extends keyof Conflictable,
|
||||||
|
> {
|
||||||
newEvent: {
|
newEvent: {
|
||||||
name: NE;
|
name: NE;
|
||||||
argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
|
argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
|
||||||
@ -241,7 +285,11 @@ export function listenToMultiple<
|
|||||||
UE extends keyof Connectable, // update Event
|
UE extends keyof Connectable, // update Event
|
||||||
RE extends keyof Connectable, // remove Event
|
RE extends keyof Connectable, // remove Event
|
||||||
CE extends keyof Conflictable, // conflict Event
|
CE extends keyof Conflictable, // conflict Event
|
||||||
>(atomEffectParam: AtomEffectParam<V>, guildId: number, eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>) {
|
>(
|
||||||
|
atomEffectParam: AtomEffectParam<V>,
|
||||||
|
guildId: number,
|
||||||
|
eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>,
|
||||||
|
) {
|
||||||
const { getPromise } = atomEffectParam;
|
const { getPromise } = atomEffectParam;
|
||||||
// listen for updates
|
// listen for updates
|
||||||
let guild: CombinedGuild | null = null;
|
let guild: CombinedGuild | null = null;
|
||||||
@ -256,9 +304,21 @@ export function listenToMultiple<
|
|||||||
if (closed) return; // make sure not to bind events if this closed while we were fetching the guild state
|
if (closed) return; // make sure not to bind events if this closed while we were fetching the guild state
|
||||||
|
|
||||||
onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc);
|
onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc);
|
||||||
onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc);
|
onUpdateFunc = createEventHandler(
|
||||||
onRemoveFunc = createEventHandler(atomEffectParam, eventMapping.removedEvent.argsMap, eventMapping.removedEvent.applyFunc);
|
atomEffectParam,
|
||||||
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc);
|
eventMapping.updatedEvent.argsMap,
|
||||||
|
eventMapping.updatedEvent.applyFunc,
|
||||||
|
);
|
||||||
|
onRemoveFunc = createEventHandler(
|
||||||
|
atomEffectParam,
|
||||||
|
eventMapping.removedEvent.argsMap,
|
||||||
|
eventMapping.removedEvent.applyFunc,
|
||||||
|
);
|
||||||
|
onConflictFunc = createEventHandler(
|
||||||
|
atomEffectParam,
|
||||||
|
eventMapping.conflictEvent.argsMap,
|
||||||
|
eventMapping.conflictEvent.applyFunc,
|
||||||
|
);
|
||||||
|
|
||||||
guild.on(eventMapping.newEvent.name, onNewFunc);
|
guild.on(eventMapping.newEvent.name, onNewFunc);
|
||||||
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
|
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
|
||||||
@ -279,7 +339,12 @@ export function guildDataSubscriptionLoadableSingleEffect<
|
|||||||
T, // e.g. GuildMetadata
|
T, // e.g. GuildMetadata
|
||||||
UE extends keyof Connectable, // update Event
|
UE extends keyof Connectable, // update Event
|
||||||
CE extends keyof Conflictable, // conflict Event
|
CE extends keyof Conflictable, // conflict Event
|
||||||
>(guildId: number, fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>, eventMapping: SingleEventMappingParams<T, LoadableValue<T>, UE, CE>, skipFunc?: () => boolean) {
|
>(
|
||||||
|
guildId: number,
|
||||||
|
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
|
||||||
|
eventMapping: SingleEventMappingParams<T, LoadableValue<T>, UE, CE>,
|
||||||
|
skipFunc?: () => boolean,
|
||||||
|
) {
|
||||||
const effect: AtomEffect<LoadableValue<T>> = (atomEffectParam: AtomEffectParam<LoadableValue<T>>) => {
|
const effect: AtomEffect<LoadableValue<T>> = (atomEffectParam: AtomEffectParam<LoadableValue<T>>) => {
|
||||||
const { trigger } = atomEffectParam;
|
const { trigger } = atomEffectParam;
|
||||||
if (skipFunc && skipFunc()) return; // don't run if this atom should be skipped for some reason (e.g. null resourceId)
|
if (skipFunc && skipFunc()) return; // don't run if this atom should be skipped for some reason (e.g. null resourceId)
|
||||||
@ -288,7 +353,13 @@ export function guildDataSubscriptionLoadableSingleEffect<
|
|||||||
|
|
||||||
// fetch initial value on first get
|
// fetch initial value on first get
|
||||||
if (trigger === 'get') {
|
if (trigger === 'get') {
|
||||||
fetchValueFunc();
|
(async () => {
|
||||||
|
try {
|
||||||
|
await fetchValueFunc();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
LOG.error('error fetching initial value', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen to changes
|
// listen to changes
|
||||||
@ -301,7 +372,13 @@ export function guildDataSubscriptionLoadableSingleEffect<
|
|||||||
return effect;
|
return effect;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function guildDataSubscriptionLoadableMultipleEffect<T extends { id: string }, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable>(
|
export function guildDataSubscriptionLoadableMultipleEffect<
|
||||||
|
T extends { id: string },
|
||||||
|
NE extends keyof Connectable,
|
||||||
|
UE extends keyof Connectable,
|
||||||
|
RE extends keyof Connectable,
|
||||||
|
CE extends keyof Conflictable,
|
||||||
|
>(
|
||||||
guildId: number,
|
guildId: number,
|
||||||
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
|
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
|
||||||
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, NE, UE, RE, CE>,
|
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, NE, UE, RE, CE>,
|
||||||
@ -313,7 +390,13 @@ export function guildDataSubscriptionLoadableMultipleEffect<T extends { id: stri
|
|||||||
|
|
||||||
// fetch initial value on first get
|
// fetch initial value on first get
|
||||||
if (trigger === 'get') {
|
if (trigger === 'get') {
|
||||||
fetchValueFunc();
|
(async () => {
|
||||||
|
try {
|
||||||
|
await fetchValueFunc();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
LOG.error('error fetching initial value', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen to changes
|
// listen to changes
|
||||||
@ -355,70 +438,84 @@ export function multipleScrollingGuildSubscriptionEffect<
|
|||||||
guildId,
|
guildId,
|
||||||
fetchCount,
|
fetchCount,
|
||||||
fetchFuncs.fetchBottomFunc,
|
fetchFuncs.fetchBottomFunc,
|
||||||
(result: T[]) => createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove), // above end
|
(result: T[]) =>
|
||||||
|
createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove), // above end
|
||||||
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow), // below end
|
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow), // below end
|
||||||
);
|
);
|
||||||
// fetch Above a Reference
|
// fetch Above a Reference
|
||||||
const { fetchValueReferenceFunc: fetchValueAboveReferenceFunc, cancel: cancelAbove } = createFetchValueScrollingReferenceFunc<T>(
|
const { fetchValueReferenceFunc: fetchValueAboveReferenceFunc, cancel: cancelAbove } =
|
||||||
atomEffectParam,
|
createFetchValueScrollingReferenceFunc<T>(
|
||||||
guildId,
|
atomEffectParam,
|
||||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.above,
|
guildId,
|
||||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, above: end }), // for "cancelled, pending, etc"
|
(selfState: LoadedValueScrolling<T[], T>) => selfState.above,
|
||||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({
|
||||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
|
||||||
let sliced = false;
|
|
||||||
if (nextValue.length > maxElements) {
|
|
||||||
nextValue = nextValue.slice(undefined, maxElements);
|
|
||||||
cancelBelow();
|
|
||||||
sliced = true;
|
|
||||||
}
|
|
||||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
|
||||||
...selfState,
|
...selfState,
|
||||||
value: nextValue,
|
|
||||||
above: end,
|
above: end,
|
||||||
below: {
|
}), // for "cancelled, pending, etc"
|
||||||
...selfState.below,
|
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||||
hasMore: sliced ? true : selfState.below.hasMore,
|
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||||
} as LoadableScrollingEnd<T>, // this is OK since selfState.below is already a LoadableScrollingEnd and we are only modifying hasMore to potentially include a boolean
|
let sliced = false;
|
||||||
};
|
if (nextValue.length > maxElements) {
|
||||||
return loadedValue;
|
nextValue = nextValue.slice(undefined, maxElements);
|
||||||
},
|
cancelBelow();
|
||||||
fetchCount,
|
sliced = true;
|
||||||
fetchFuncs.fetchAboveFunc,
|
}
|
||||||
);
|
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||||
|
...selfState,
|
||||||
|
value: nextValue,
|
||||||
|
above: end,
|
||||||
|
below: {
|
||||||
|
...selfState.below,
|
||||||
|
hasMore: sliced ? true : selfState.below.hasMore,
|
||||||
|
} as LoadableScrollingEnd<T>, // this is OK since selfState.below is already a LoadableScrollingEnd and we are only modifying hasMore to potentially include a boolean
|
||||||
|
};
|
||||||
|
return loadedValue;
|
||||||
|
},
|
||||||
|
fetchCount,
|
||||||
|
fetchFuncs.fetchAboveFunc,
|
||||||
|
);
|
||||||
// fetch Below a Reference
|
// fetch Below a Reference
|
||||||
const { fetchValueReferenceFunc: fetchValueBelowReferenceFunc, cancel: cancelBelow } = createFetchValueScrollingReferenceFunc<T>(
|
const { fetchValueReferenceFunc: fetchValueBelowReferenceFunc, cancel: cancelBelow } =
|
||||||
atomEffectParam,
|
createFetchValueScrollingReferenceFunc<T>(
|
||||||
guildId,
|
atomEffectParam,
|
||||||
(selfState: LoadedValueScrolling<T[], T>) => selfState.below,
|
guildId,
|
||||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, below: end }),
|
(selfState: LoadedValueScrolling<T[], T>) => selfState.below,
|
||||||
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({
|
||||||
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
|
||||||
let sliced = false;
|
|
||||||
if (nextValue.length > maxElements) {
|
|
||||||
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
|
|
||||||
cancelAbove();
|
|
||||||
sliced = true;
|
|
||||||
}
|
|
||||||
const loadedValue: LoadedValueScrolling<T[], T> = {
|
|
||||||
...selfState,
|
...selfState,
|
||||||
value: nextValue,
|
|
||||||
above: {
|
|
||||||
...selfState.above,
|
|
||||||
hasMore: sliced ? true : selfState.above.hasMore,
|
|
||||||
} as LoadableScrollingEnd<T>,
|
|
||||||
below: end,
|
below: end,
|
||||||
};
|
}),
|
||||||
return loadedValue;
|
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
|
||||||
},
|
let nextValue = result.concat(selfState.value).sort(sortFunc);
|
||||||
fetchCount,
|
let sliced = false;
|
||||||
fetchFuncs.fetchBelowFunc,
|
if (nextValue.length > maxElements) {
|
||||||
);
|
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
|
||||||
|
cancelAbove();
|
||||||
|
sliced = true;
|
||||||
|
}
|
||||||
|
const loadedValue: LoadedValueScrolling<T[], T> = {
|
||||||
|
...selfState,
|
||||||
|
value: nextValue,
|
||||||
|
above: {
|
||||||
|
...selfState.above,
|
||||||
|
hasMore: sliced ? true : selfState.above.hasMore,
|
||||||
|
} as LoadableScrollingEnd<T>,
|
||||||
|
below: end,
|
||||||
|
};
|
||||||
|
return loadedValue;
|
||||||
|
},
|
||||||
|
fetchCount,
|
||||||
|
fetchFuncs.fetchBelowFunc,
|
||||||
|
);
|
||||||
|
|
||||||
// fetch bottom value on first get
|
// fetch bottom value on first get
|
||||||
if (trigger === 'get') {
|
if (trigger === 'get') {
|
||||||
LOG.debug('fetching scrolling bottom...');
|
(async () => {
|
||||||
fetchValueBottomFunc();
|
try {
|
||||||
|
await fetchValueBottomFunc();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
LOG.error('error fetching bottom value', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen to changes
|
// listen to changes
|
||||||
@ -434,19 +531,33 @@ export function multipleScrollingGuildSubscriptionEffect<
|
|||||||
export function applyNewElements<T>(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
|
export function applyNewElements<T>(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
|
||||||
return list.concat(newElements).sort(sortFunc);
|
return list.concat(newElements).sort(sortFunc);
|
||||||
}
|
}
|
||||||
export function applyUpdatedElements<T extends { id: string }>(list: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] {
|
export function applyUpdatedElements<T extends { id: string }>(
|
||||||
return list.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
|
list: T[],
|
||||||
|
updatedElements: T[],
|
||||||
|
sortFunc: (a: T, b: T) => number,
|
||||||
|
): T[] {
|
||||||
|
return list
|
||||||
|
.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element)
|
||||||
|
.sort(sortFunc);
|
||||||
}
|
}
|
||||||
export function applyRemovedElements<T extends { id: string }>(list: T[], removedElements: T[]): T[] {
|
export function applyRemovedElements<T extends { id: string }>(list: T[], removedElements: T[]): T[] {
|
||||||
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id));
|
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id));
|
||||||
return list.filter(element => !removedIds.has(element.id));
|
return list.filter(element => !removedIds.has(element.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyChangedElements<T extends { id: string }>(list: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
|
export function applyChangedElements<T extends { id: string }>(
|
||||||
|
list: T[],
|
||||||
|
changes: Changes<T>,
|
||||||
|
sortFunc: (a: T, b: T) => number,
|
||||||
|
): T[] {
|
||||||
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id));
|
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id));
|
||||||
return list
|
return list
|
||||||
.concat(changes.added)
|
.concat(changes.added)
|
||||||
.map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element)
|
.map(
|
||||||
|
element =>
|
||||||
|
changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ??
|
||||||
|
element,
|
||||||
|
)
|
||||||
.filter(element => !removedIds.has(element.id))
|
.filter(element => !removedIds.has(element.id))
|
||||||
.sort(sortFunc);
|
.sort(sortFunc);
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ export default class ElementsUtil {
|
|||||||
return src;
|
return src;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
LOG.warn('unable to fetch and convert guild resource, showing error instead', e);
|
LOG.warn('unable to fetch and convert guild resource, showing error instead', e);
|
||||||
|
//xLOG.warn('unable to fetch and convert guild resource, showing error instead', e);
|
||||||
return './img/error.png';
|
return './img/error.png';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,20 @@ 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, Dispatch, MutableRefObject, ReactNode, RefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
DependencyList,
|
||||||
|
Dispatch,
|
||||||
|
MutableRefObject,
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
|
UIEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { ShouldNeverHappenError } from '../../data-types';
|
import { ShouldNeverHappenError } from '../../data-types';
|
||||||
import Util from '../../util';
|
import Util from '../../util';
|
||||||
import Globals from '../../globals';
|
import Globals from '../../globals';
|
||||||
@ -46,7 +59,11 @@ function useShake(ms: number): [shaking: boolean, doShake: () => void] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runs an Async action one time (updates when deps changes)
|
// runs an Async action one time (updates when deps changes)
|
||||||
export function useOneTimeAsyncAction<T, V>(actionFunc: () => Promise<T>, initialValue: V, deps: DependencyList): [value: T | V, error: unknown | null] {
|
export function useOneTimeAsyncAction<T, V>(
|
||||||
|
actionFunc: () => Promise<T>,
|
||||||
|
initialValue: V,
|
||||||
|
deps: DependencyList,
|
||||||
|
): [value: T | V, error: unknown | null] {
|
||||||
const isMounted = useRef(false);
|
const isMounted = useRef(false);
|
||||||
|
|
||||||
const [value, setValue] = useState<T | V>(initialValue);
|
const [value, setValue] = useState<T | V>(initialValue);
|
||||||
@ -79,9 +96,11 @@ export function useOneTimeAsyncAction<T, V>(actionFunc: () => Promise<T>, initia
|
|||||||
|
|
||||||
/** creates a callable async function that will not double-trigger and gives a result, error message, and shaking boolean */
|
/** creates a callable async function that will not double-trigger and gives a result, error message, and shaking boolean */
|
||||||
export function useAsyncCallback<ResultType>(
|
export function useAsyncCallback<ResultType>(
|
||||||
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
actionFunc: (
|
||||||
|
isMounted: MutableRefObject<boolean>,
|
||||||
|
) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
||||||
deps: DependencyList,
|
deps: DependencyList,
|
||||||
): [callable: () => void, result: ResultType | null, errorMessage: string | null, shaking: boolean] {
|
): [callable: () => void, result: ResultType | null, errorMessage: string | null, shaking: boolean, pending: boolean] {
|
||||||
const isMounted = useIsMountedRef();
|
const isMounted = useIsMountedRef();
|
||||||
|
|
||||||
const [result, setResult] = useState<ResultType | null>(null);
|
const [result, setResult] = useState<ResultType | null>(null);
|
||||||
@ -103,20 +122,23 @@ export function useAsyncCallback<ResultType>(
|
|||||||
setPending(false);
|
setPending(false);
|
||||||
if (errorMessage) doShake();
|
if (errorMessage) doShake();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
LOG.error('unable to perform submit button actionFunc', e);
|
LOG.error('unable to perform async callback', e);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
setErrorMessage('Unknown error');
|
setErrorMessage('unknown error');
|
||||||
if (errorMessage) doShake();
|
doShake();
|
||||||
setPending(false);
|
setPending(false);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMounted, pending, actionFunc, doShake, errorMessage, ...deps]);
|
}, [isMounted, pending, actionFunc, doShake, errorMessage, ...deps]);
|
||||||
|
|
||||||
return [callable, result, errorMessage, shaking];
|
return [callable, result, errorMessage, shaking, pending];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** creates a callable async function that does not need to return a value (or shake) */
|
/** creates a callable async function that does not need to return a value (or shake) */
|
||||||
export function useAsyncVoidCallback(actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<void>, deps: DependencyList): [callable: () => void] {
|
export function useAsyncVoidCallback(
|
||||||
|
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<void>,
|
||||||
|
deps: DependencyList,
|
||||||
|
): [callable: () => void] {
|
||||||
const [callable] = useAsyncCallback(async (isMounted: MutableRefObject<boolean>) => {
|
const [callable] = useAsyncCallback(async (isMounted: MutableRefObject<boolean>) => {
|
||||||
await actionFunc(isMounted);
|
await actionFunc(isMounted);
|
||||||
return { errorMessage: null, result: null };
|
return { errorMessage: null, result: null };
|
||||||
@ -129,9 +151,28 @@ export function useDownloadButton(
|
|||||||
downloadName: string,
|
downloadName: string,
|
||||||
fetchBuff: () => Promise<Buffer | null>,
|
fetchBuff: () => Promise<Buffer | null>,
|
||||||
fetchBuffDeps: DependencyList,
|
fetchBuffDeps: DependencyList,
|
||||||
stateTextMapping?: { start?: string; pendingFetch?: string; errorFetch?: string; notReadyFetch?: string; pendingSave?: string; errorSave?: string; success?: string },
|
stateTextMapping?: {
|
||||||
|
start?: string;
|
||||||
|
pendingFetch?: string;
|
||||||
|
errorFetch?: string;
|
||||||
|
notReadyFetch?: string;
|
||||||
|
pendingSave?: string;
|
||||||
|
errorSave?: string;
|
||||||
|
success?: string;
|
||||||
|
},
|
||||||
): [callable: () => void, text: string, shaking: boolean] {
|
): [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 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 [filePath, setFilePath] = useState<string | null>(null);
|
||||||
const [fileBuffer, setFileBuffer] = useState<Buffer | null>(null);
|
const [fileBuffer, setFileBuffer] = useState<Buffer | null>(null);
|
||||||
@ -207,30 +248,25 @@ export function useDownloadButton(
|
|||||||
|
|
||||||
/** creates useful state variables for a button intended call a submit action */
|
/** creates useful state variables for a button intended call a submit action */
|
||||||
export function useAsyncSubmitButton<ResultType>(
|
export function useAsyncSubmitButton<ResultType>(
|
||||||
actionFunc: (isMounted: MutableRefObject<boolean>) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
actionFunc: (
|
||||||
|
isMounted: MutableRefObject<boolean>,
|
||||||
|
) => Promise<{ errorMessage: string | null; result: ResultType | null }>,
|
||||||
deps: DependencyList,
|
deps: DependencyList,
|
||||||
stateTextMapping?: { start?: string; pending?: string; error?: string; done?: string },
|
stateTextMapping?: { start?: string; pending?: string; error?: string; done?: string },
|
||||||
): [callable: () => void, text: string, shaking: boolean, result: ResultType | null, errorMessage: string | null] {
|
): [callable: () => void, text: string, shaking: boolean, result: ResultType | null, errorMessage: string | null] {
|
||||||
const textMapping = { ...{ start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' }, ...stateTextMapping };
|
const textMapping = {
|
||||||
|
...{ start: 'Submit', pending: 'Submitting...', error: 'Try Again', done: 'Done' },
|
||||||
|
...stateTextMapping,
|
||||||
|
};
|
||||||
|
|
||||||
const [pending, setPending] = useState<boolean>(false);
|
|
||||||
const [complete, setComplete] = useState<boolean>(false);
|
const [complete, setComplete] = useState<boolean>(false);
|
||||||
|
|
||||||
const [callable, result, errorMessage, shaking] = useAsyncCallback(
|
const [callable, result, errorMessage, shaking, pending] = useAsyncCallback(
|
||||||
async (isMounted: MutableRefObject<boolean>) => {
|
async (isMounted: MutableRefObject<boolean>) => {
|
||||||
try {
|
const actionReturnValue = await actionFunc(isMounted);
|
||||||
setPending(true);
|
if (!isMounted) return { errorMessage: null, result: null };
|
||||||
const actionReturnValue = await actionFunc(isMounted);
|
if (actionReturnValue.errorMessage === null) setComplete(true);
|
||||||
if (!isMounted) return { errorMessage: null, result: null };
|
return actionReturnValue;
|
||||||
setPending(false);
|
|
||||||
if (actionReturnValue.errorMessage === null) setComplete(true);
|
|
||||||
return actionReturnValue;
|
|
||||||
} catch (e) {
|
|
||||||
LOG.error('Error submitting button', e);
|
|
||||||
if (!isMounted) return { errorMessage: null, result: null };
|
|
||||||
setPending(false);
|
|
||||||
return { errorMessage: 'Error submitting button', result: null };
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[...deps, actionFunc],
|
[...deps, actionFunc],
|
||||||
);
|
);
|
||||||
@ -325,9 +361,15 @@ export function useScrollableCallables<T>(
|
|||||||
|
|
||||||
// makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside
|
// makes sure to also allow you to fly-out a click starting inside of the ref'd element but was dragged outside
|
||||||
/** calls the close action when you hit escape or click outside of the ref element */
|
/** calls the close action when you hit escape or click outside of the ref element */
|
||||||
export function useActionWhenEscapeOrClickedOrContextOutsideEffect(ref: RefObject<HTMLElement>, actionFunc: () => void) {
|
export function useActionWhenEscapeOrClickedOrContextOutsideEffect(
|
||||||
|
ref: RefObject<HTMLElement>,
|
||||||
|
actionFunc: () => void,
|
||||||
|
) {
|
||||||
// have to use a ref here and not states since we can't re-assign state between mouseup and click
|
// have to use a ref here and not states since we can't re-assign state between mouseup and click
|
||||||
const mouseRef = useRef<{ mouseDownTarget: Node | null; mouseUpTarget: Node | null }>({ mouseDownTarget: null, mouseUpTarget: null });
|
const mouseRef = useRef<{ mouseDownTarget: Node | null; mouseUpTarget: Node | null }>({
|
||||||
|
mouseDownTarget: null,
|
||||||
|
mouseUpTarget: null,
|
||||||
|
});
|
||||||
|
|
||||||
const handleClickOutside = useCallback(
|
const handleClickOutside = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
@ -453,7 +495,10 @@ export function useAlignment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** creates useful context menu state */
|
/** creates useful context menu state */
|
||||||
export function useContextMenu(createContextMenu: (close: () => void) => ReactNode, createContextMenuDeps: DependencyList): [contextMenu: ReactNode, toggle: () => void, close: () => void, open: () => void, isOpen: boolean] {
|
export function useContextMenu(
|
||||||
|
createContextMenu: (close: () => void) => ReactNode,
|
||||||
|
createContextMenuDeps: DependencyList,
|
||||||
|
): [contextMenu: ReactNode, toggle: () => void, close: () => void, open: () => void, isOpen: boolean] {
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
@ -482,9 +527,15 @@ export function useContextClickContextMenu(
|
|||||||
|
|
||||||
const alignment = useMemo(() => ({ top: 'centerY', left: 'centerX' }), []);
|
const alignment = useMemo(() => ({ top: 'centerY', left: 'centerX' }), []);
|
||||||
|
|
||||||
const createContextMenuWithRelativeToPos = useCallback((close: () => void) => createContextMenu(alignment, relativeToPos, close), [alignment, createContextMenu, relativeToPos]);
|
const createContextMenuWithRelativeToPos = useCallback(
|
||||||
|
(close: () => void) => createContextMenu(alignment, relativeToPos, close),
|
||||||
|
[alignment, createContextMenu, relativeToPos],
|
||||||
|
);
|
||||||
|
|
||||||
const [contextMenu, _toggle, _close, open] = useContextMenu(createContextMenuWithRelativeToPos, createContextMenuDeps);
|
const [contextMenu, _toggle, _close, open] = useContextMenu(
|
||||||
|
createContextMenuWithRelativeToPos,
|
||||||
|
createContextMenuDeps,
|
||||||
|
);
|
||||||
|
|
||||||
const onContextMenu = useCallback(
|
const onContextMenu = useCallback(
|
||||||
(event: React.MouseEvent<HTMLElement>) => {
|
(event: React.MouseEvent<HTMLElement>) => {
|
||||||
@ -498,7 +549,10 @@ export function useContextClickContextMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** creates context state for hovering over an element */
|
/** creates context state for hovering over an element */
|
||||||
export function useContextHover(createContextHover: () => ReactNode, createContextHoverDeps: DependencyList): [contextHover: ReactNode, mouseEnterCallable: () => void, mouseLeaveCallable: () => void] {
|
export function useContextHover(
|
||||||
|
createContextHover: () => ReactNode,
|
||||||
|
createContextHoverDeps: DependencyList,
|
||||||
|
): [contextHover: ReactNode, mouseEnterCallable: () => void, mouseLeaveCallable: () => void] {
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const contextHover = useMemo(
|
const contextHover = useMemo(
|
||||||
@ -518,7 +572,11 @@ export function useContextHover(createContextHover: () => ReactNode, createConte
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** creates a drop target element that will appear when you drag a file over the document */
|
/** creates a drop target element that will appear when you drag a file over the document */
|
||||||
export function useDocumentDropTarget(message: string, setBuff: Dispatch<SetStateAction<Buffer | null>>, setName: Dispatch<SetStateAction<string | null>>): [dropTarget: ReactNode] {
|
export function useDocumentDropTarget(
|
||||||
|
message: string,
|
||||||
|
setBuff: Dispatch<SetStateAction<Buffer | null>>,
|
||||||
|
setName: Dispatch<SetStateAction<string | null>>,
|
||||||
|
): [dropTarget: ReactNode] {
|
||||||
const [dragEnabled, setDragEnabled] = useState<boolean>(false);
|
const [dragEnabled, setDragEnabled] = useState<boolean>(false);
|
||||||
|
|
||||||
// drag overlay
|
// drag overlay
|
||||||
|
@ -7,7 +7,18 @@ import * as socketio from 'socket.io-client';
|
|||||||
import PersonalDBGuild from './guild-personal-db';
|
import PersonalDBGuild from './guild-personal-db';
|
||||||
import RAMGuild from './guild-ram';
|
import RAMGuild from './guild-ram';
|
||||||
import SocketGuild from './guild-socket';
|
import SocketGuild from './guild-socket';
|
||||||
import { Changes, Channel, ConnectionInfo, GuildMetadata, GuildMetadataLocal, 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 MessageRAMCache from './message-ram-cache';
|
||||||
import PersonalDB from './personal-db';
|
import PersonalDB from './personal-db';
|
||||||
@ -19,9 +30,13 @@ import EnsuredFetchable from './fetchable-ensured';
|
|||||||
import { EventEmitter } from 'tsee';
|
import { EventEmitter } from 'tsee';
|
||||||
import { AutoVerifierChangesType } from './auto-verifier';
|
import { AutoVerifierChangesType } from './auto-verifier';
|
||||||
import { IDQuery, PartialMessageListQuery } from './auto-verifier-with-args';
|
import { IDQuery, PartialMessageListQuery } from './auto-verifier-with-args';
|
||||||
|
import Util from './util';
|
||||||
|
|
||||||
/** the general guild class. This handles connecting a RAM, PersonalDB, and Socket guild together using fetchable-pair-verifiers and some manual caching to nicely update messages */
|
/** the general guild class. This handles connecting a RAM, PersonalDB, and Socket guild together using fetchable-pair-verifiers and some manual caching to nicely update messages */
|
||||||
export default class CombinedGuild extends EventEmitter<Connectable & Conflictable> implements AsyncGuaranteedFetchable, AsyncRequestable {
|
export default class CombinedGuild
|
||||||
|
extends EventEmitter<Connectable & Conflictable>
|
||||||
|
implements AsyncGuaranteedFetchable, AsyncRequestable
|
||||||
|
{
|
||||||
private readonly ramGuild: RAMGuild;
|
private readonly ramGuild: RAMGuild;
|
||||||
private readonly personalDBGuild: PersonalDBGuild;
|
private readonly personalDBGuild: PersonalDBGuild;
|
||||||
private readonly socketGuild: SocketGuild;
|
private readonly socketGuild: SocketGuild;
|
||||||
@ -29,7 +44,15 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
private readonly fetchable: AsyncGuaranteedFetchable;
|
private readonly fetchable: AsyncGuaranteedFetchable;
|
||||||
private readonly mainPairVerifier: PairVerifierFetchable;
|
private readonly mainPairVerifier: PairVerifierFetchable;
|
||||||
|
|
||||||
constructor(public readonly id: number, public readonly memberId: string, socket: socketio.Socket, socketVerifier: SocketVerifier, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB) {
|
constructor(
|
||||||
|
public readonly id: number,
|
||||||
|
public readonly memberId: string,
|
||||||
|
socket: socketio.Socket,
|
||||||
|
socketVerifier: SocketVerifier,
|
||||||
|
messageRAMCache: MessageRAMCache,
|
||||||
|
resourceRAMCache: ResourceRAMCache,
|
||||||
|
personalDB: PersonalDB,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.ramGuild = new RAMGuild(messageRAMCache, resourceRAMCache, this.id);
|
this.ramGuild = new RAMGuild(messageRAMCache, resourceRAMCache, this.id);
|
||||||
@ -102,8 +125,18 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
LOG.info(`g#${this.id} ${message}`);
|
LOG.info(`g#${this.id} ${message}`);
|
||||||
}
|
}
|
||||||
this.ramGuild.handleMessagesAdded(messages, diskSocket.fetchMessagesRecentVerifier, diskSocket.fetchMessagesBeforeVerifier, diskSocket.fetchMessagesAfterVerifier);
|
this.ramGuild.handleMessagesAdded(
|
||||||
await this.personalDBGuild.handleMessagesAdded(messages, diskSocket.fetchMessagesRecentVerifier, diskSocket.fetchMessagesBeforeVerifier, diskSocket.fetchMessagesAfterVerifier);
|
messages,
|
||||||
|
diskSocket.fetchMessagesRecentVerifier,
|
||||||
|
diskSocket.fetchMessagesBeforeVerifier,
|
||||||
|
diskSocket.fetchMessagesAfterVerifier,
|
||||||
|
);
|
||||||
|
await this.personalDBGuild.handleMessagesAdded(
|
||||||
|
messages,
|
||||||
|
diskSocket.fetchMessagesRecentVerifier,
|
||||||
|
diskSocket.fetchMessagesBeforeVerifier,
|
||||||
|
diskSocket.fetchMessagesAfterVerifier,
|
||||||
|
);
|
||||||
const members = await this.grabRAMMembersMap();
|
const members = await this.grabRAMMembersMap();
|
||||||
const channels = await this.grabRAMChannelsMap();
|
const channels = await this.grabRAMChannelsMap();
|
||||||
for (const message of messages) message.fill(members, channels);
|
for (const message of messages) message.fill(members, channels);
|
||||||
@ -113,8 +146,18 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
LOG.info(`g#${this.id} updated ${message}`);
|
LOG.info(`g#${this.id} updated ${message}`);
|
||||||
}
|
}
|
||||||
this.ramGuild.handleMessagesChanged(messages, ramDiskSocket.fetchMessagesRecentVerifier, ramDiskSocket.fetchMessagesBeforeVerifier, ramDiskSocket.fetchMessagesAfterVerifier);
|
this.ramGuild.handleMessagesChanged(
|
||||||
await this.personalDBGuild.handleMessagesChanged(messages, diskSocket.fetchMessagesRecentVerifier, diskSocket.fetchMessagesBeforeVerifier, diskSocket.fetchMessagesAfterVerifier);
|
messages,
|
||||||
|
ramDiskSocket.fetchMessagesRecentVerifier,
|
||||||
|
ramDiskSocket.fetchMessagesBeforeVerifier,
|
||||||
|
ramDiskSocket.fetchMessagesAfterVerifier,
|
||||||
|
);
|
||||||
|
await this.personalDBGuild.handleMessagesChanged(
|
||||||
|
messages,
|
||||||
|
diskSocket.fetchMessagesRecentVerifier,
|
||||||
|
diskSocket.fetchMessagesBeforeVerifier,
|
||||||
|
diskSocket.fetchMessagesAfterVerifier,
|
||||||
|
);
|
||||||
const members = await this.grabRAMMembersMap();
|
const members = await this.grabRAMMembersMap();
|
||||||
const channels = await this.grabRAMChannelsMap();
|
const channels = await this.grabRAMChannelsMap();
|
||||||
for (const message of messages) message.fill(members, channels);
|
for (const message of messages) message.fill(members, channels);
|
||||||
@ -156,10 +199,13 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
});
|
});
|
||||||
|
|
||||||
// forward the conflict events from the last verifier in the chain
|
// forward the conflict events from the last verifier in the chain
|
||||||
ramDiskSocket.on('conflict-metadata', (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => {
|
ramDiskSocket.on(
|
||||||
LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta });
|
'conflict-metadata',
|
||||||
this.emit('conflict-metadata', changesType, oldGuildMeta, newGuildMeta);
|
(changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => {
|
||||||
});
|
LOG.info(`g#${this.id} metadata conflict`, { oldGuildMeta, newGuildMeta });
|
||||||
|
this.emit('conflict-metadata', changesType, oldGuildMeta, newGuildMeta);
|
||||||
|
},
|
||||||
|
);
|
||||||
ramDiskSocket.on('conflict-members', (changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
|
ramDiskSocket.on('conflict-members', (changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
|
||||||
LOG.info(`g#${this.id} members conflict`); // this should be pretty common since we manually update the status to "unknown" which will cause a conflict
|
LOG.info(`g#${this.id} members conflict`); // this should be pretty common since we manually update the status to "unknown" which will cause a conflict
|
||||||
this.emit('conflict-members', changesType, changes);
|
this.emit('conflict-members', changesType, changes);
|
||||||
@ -168,28 +214,40 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
LOG.info(`g#${this.id} channels conflict`, { changes });
|
LOG.info(`g#${this.id} channels conflict`, { changes });
|
||||||
this.emit('conflict-channels', changesType, changes);
|
this.emit('conflict-channels', changesType, changes);
|
||||||
});
|
});
|
||||||
ramDiskSocket.on('conflict-messages', async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => {
|
ramDiskSocket.on(
|
||||||
const members = await this.grabRAMMembersMap();
|
'conflict-messages',
|
||||||
const channels = await this.grabRAMChannelsMap();
|
async (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => {
|
||||||
for (const message of changes.added) message.fill(members, channels);
|
const members = await this.grabRAMMembersMap();
|
||||||
for (const dataPoint of changes.updated) dataPoint.newDataPoint.fill(members, channels);
|
const channels = await this.grabRAMChannelsMap();
|
||||||
for (const message of changes.deleted) message.fill(members, channels);
|
for (const message of changes.added) message.fill(members, channels);
|
||||||
this.emit('conflict-messages', query, changesType, changes);
|
for (const dataPoint of changes.updated) dataPoint.newDataPoint.fill(members, channels);
|
||||||
});
|
for (const message of changes.deleted) message.fill(members, channels);
|
||||||
|
this.emit('conflict-messages', query, changesType, changes);
|
||||||
|
},
|
||||||
|
);
|
||||||
ramDiskSocket.on('conflict-tokens', (changesType: AutoVerifierChangesType, changes: Changes<Token>) => {
|
ramDiskSocket.on('conflict-tokens', (changesType: AutoVerifierChangesType, changes: Changes<Token>) => {
|
||||||
LOG.info(`g#${this.id} tokens conflict`, { changes });
|
LOG.info(`g#${this.id} tokens conflict`, { changes });
|
||||||
this.emit('conflict-tokens', changesType, changes);
|
this.emit('conflict-tokens', changesType, changes);
|
||||||
});
|
});
|
||||||
ramDiskSocket.on('conflict-resource', (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => {
|
ramDiskSocket.on(
|
||||||
LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource });
|
'conflict-resource',
|
||||||
this.emit('conflict-resource', query, changesType, oldResource, newResource);
|
(query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => {
|
||||||
});
|
LOG.warn(`g#${this.id} resource conflict`, { oldResource, newResource });
|
||||||
|
this.emit('conflict-resource', query, changesType, oldResource, newResource);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.fetchable = new EnsuredFetchable(ramDiskSocket);
|
this.fetchable = new EnsuredFetchable(ramDiskSocket);
|
||||||
this.mainPairVerifier = ramDiskSocket;
|
this.mainPairVerifier = ramDiskSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async create(guildMetadata: GuildMetadataLocal, socketConfig: SocketConfig, messageRAMCache: MessageRAMCache, resourceRAMCache: ResourceRAMCache, personalDB: PersonalDB) {
|
static async create(
|
||||||
|
guildMetadata: GuildMetadataLocal,
|
||||||
|
socketConfig: SocketConfig,
|
||||||
|
messageRAMCache: MessageRAMCache,
|
||||||
|
resourceRAMCache: ResourceRAMCache,
|
||||||
|
personalDB: PersonalDB,
|
||||||
|
) {
|
||||||
if (guildMetadata.memberId === null) {
|
if (guildMetadata.memberId === null) {
|
||||||
throw new Error('trying to launch guild that we have never verified with');
|
throw new Error('trying to launch guild that we have never verified with');
|
||||||
}
|
}
|
||||||
@ -198,7 +256,15 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
ca: socketConfig.cert,
|
ca: socketConfig.cert,
|
||||||
});
|
});
|
||||||
const socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey);
|
const socketVerifier = new SocketVerifier(socket, socketConfig.publicKey, socketConfig.privateKey);
|
||||||
return new CombinedGuild(guildMetadata.id, guildMetadata.memberId, socket, socketVerifier, messageRAMCache, resourceRAMCache, personalDB);
|
return new CombinedGuild(
|
||||||
|
guildMetadata.id,
|
||||||
|
guildMetadata.memberId,
|
||||||
|
socket,
|
||||||
|
socketVerifier,
|
||||||
|
messageRAMCache,
|
||||||
|
resourceRAMCache,
|
||||||
|
personalDB,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSocketVerified(): boolean {
|
public isSocketVerified(): boolean {
|
||||||
@ -220,7 +286,8 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
if (this.ramGuild.getChannels().size === 0) {
|
if (this.ramGuild.getChannels().size === 0) {
|
||||||
await this.fetchChannels();
|
await this.fetchChannels();
|
||||||
}
|
}
|
||||||
if (this.ramGuild.getChannels().size === 0) throw new Error('RAM Channels was not updated through fetchChannels');
|
if (this.ramGuild.getChannels().size === 0)
|
||||||
|
throw new Error('RAM Channels was not updated through fetchChannels');
|
||||||
}
|
}
|
||||||
|
|
||||||
async grabRAMMembersMap(): Promise<Map<string, Member>> {
|
async grabRAMMembersMap(): Promise<Map<string, Member>> {
|
||||||
@ -261,7 +328,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
connection.rolePriority = member.rolePriority;
|
connection.rolePriority = member.rolePriority;
|
||||||
connection.privileges = member.privileges;
|
connection.privileges = member.privileges;
|
||||||
} else {
|
} else {
|
||||||
LOG.warn(`g#${this.id} unable to find self in members (of ${members.size}, [${Array.from(members.keys()).join(',')}], ${member}, ${this.memberId})`);
|
LOG.warn(
|
||||||
|
`g#${this.id} unable to find self in members (of ${members.size}, [${Array.from(
|
||||||
|
members.keys(),
|
||||||
|
).join(',')}], ${member}, ${this.memberId})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const members = await this.personalDBGuild.fetchMembers();
|
const members = await this.personalDBGuild.fetchMembers();
|
||||||
@ -283,15 +354,19 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
|
|
||||||
// fetched through the triple-cache system (RAM -> Disk -> Server)
|
// fetched through the triple-cache system (RAM -> Disk -> Server)
|
||||||
async fetchMetadata(): Promise<GuildMetadata> {
|
async fetchMetadata(): Promise<GuildMetadata> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
return await this.fetchable.fetchMetadata();
|
return await this.fetchable.fetchMetadata();
|
||||||
}
|
}
|
||||||
async fetchMembers(): Promise<Member[]> {
|
async fetchMembers(): Promise<Member[]> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
return await this.fetchable.fetchMembers();
|
return await this.fetchable.fetchMembers();
|
||||||
}
|
}
|
||||||
async fetchChannels(): Promise<Channel[]> {
|
async fetchChannels(): Promise<Channel[]> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
return await this.fetchable.fetchChannels();
|
return await this.fetchable.fetchChannels();
|
||||||
}
|
}
|
||||||
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[]> {
|
async fetchMessagesRecent(channelId: string, number: number): Promise<Message[]> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
const members = await this.grabRAMMembersMap();
|
const members = await this.grabRAMMembersMap();
|
||||||
const channels = await this.grabRAMChannelsMap();
|
const channels = await this.grabRAMChannelsMap();
|
||||||
const messages = await this.fetchable.fetchMessagesRecent(channelId, number);
|
const messages = await this.fetchable.fetchMessagesRecent(channelId, number);
|
||||||
@ -301,6 +376,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
async fetchMessagesBefore(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
|
async fetchMessagesBefore(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
|
||||||
|
Util.failSometimes(0.5); // for testing
|
||||||
const members = await this.grabRAMMembersMap();
|
const members = await this.grabRAMMembersMap();
|
||||||
const channels = await this.grabRAMChannelsMap();
|
const channels = await this.grabRAMChannelsMap();
|
||||||
const messages = await this.fetchable.fetchMessagesBefore(channelId, messageOrderId, number);
|
const messages = await this.fetchable.fetchMessagesBefore(channelId, messageOrderId, number);
|
||||||
@ -310,6 +386,7 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
async fetchMessagesAfter(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
|
async fetchMessagesAfter(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
const members = await this.grabRAMMembersMap();
|
const members = await this.grabRAMMembersMap();
|
||||||
const channels = await this.grabRAMChannelsMap();
|
const channels = await this.grabRAMChannelsMap();
|
||||||
const messages = await this.fetchable.fetchMessagesAfter(channelId, messageOrderId, number);
|
const messages = await this.fetchable.fetchMessagesAfter(channelId, messageOrderId, number);
|
||||||
@ -319,9 +396,11 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
async fetchResource(resourceId: string): Promise<Resource> {
|
async fetchResource(resourceId: string): Promise<Resource> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
return await this.fetchable.fetchResource(resourceId);
|
return await this.fetchable.fetchResource(resourceId);
|
||||||
}
|
}
|
||||||
async fetchTokens(): Promise<Token[]> {
|
async fetchTokens(): Promise<Token[]> {
|
||||||
|
// xUtil.failSometimes(0.05); // for testing
|
||||||
const members = await this.grabRAMMembersMap();
|
const members = await this.grabRAMMembersMap();
|
||||||
const tokens = await this.fetchable.fetchTokens();
|
const tokens = await this.fetchable.fetchTokens();
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
@ -335,7 +414,12 @@ export default class CombinedGuild extends EventEmitter<Connectable & Conflictab
|
|||||||
await this.socketGuild.requestSendMessage(channelId, text);
|
await this.socketGuild.requestSendMessage(channelId, text);
|
||||||
}
|
}
|
||||||
// TODO: Change to "withAttachment"
|
// TODO: Change to "withAttachment"
|
||||||
async requestSendMessageWithResource(channelId: string, text: string | null, resource: Buffer, resourceName: string): Promise<void> {
|
async requestSendMessageWithResource(
|
||||||
|
channelId: string,
|
||||||
|
text: string | null,
|
||||||
|
resource: Buffer,
|
||||||
|
resourceName: string,
|
||||||
|
): Promise<void> {
|
||||||
await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName);
|
await this.socketGuild.requestSendMessageWithResource(channelId, text, resource, resourceName);
|
||||||
}
|
}
|
||||||
async requestSetStatus(status: string): Promise<void> {
|
async requestSetStatus(status: string): Promise<void> {
|
||||||
|
@ -32,7 +32,32 @@ export default class Util {
|
|||||||
// solution: Replace reserved characters with empty string (''), bad characters with '_', and append '_' to bad names
|
// solution: Replace reserved characters with empty string (''), bad characters with '_', and append '_' to bad names
|
||||||
|
|
||||||
// illegal File Names (Windows)
|
// illegal File Names (Windows)
|
||||||
if (['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'].indexOf(name) !== -1) {
|
if (
|
||||||
|
[
|
||||||
|
'CON',
|
||||||
|
'PRN',
|
||||||
|
'AUX',
|
||||||
|
'NUL',
|
||||||
|
'COM1',
|
||||||
|
'COM2',
|
||||||
|
'COM3',
|
||||||
|
'COM4',
|
||||||
|
'COM5',
|
||||||
|
'COM6',
|
||||||
|
'COM7',
|
||||||
|
'COM8',
|
||||||
|
'COM9',
|
||||||
|
'LPT1',
|
||||||
|
'LPT2',
|
||||||
|
'LPT3',
|
||||||
|
'LPT4',
|
||||||
|
'LPT5',
|
||||||
|
'LPT6',
|
||||||
|
'LPT7',
|
||||||
|
'LPT8',
|
||||||
|
'LPT9',
|
||||||
|
].indexOf(name) !== -1
|
||||||
|
) {
|
||||||
// TODO: case insensitive?
|
// TODO: case insensitive?
|
||||||
name += '_';
|
name += '_';
|
||||||
}
|
}
|
||||||
@ -72,6 +97,11 @@ export default class Util {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** throws an error based on a ratio from 0-1 */
|
||||||
|
static failSometimes(ratio: number): void {
|
||||||
|
if (Math.random() < ratio) throw new Error('fail sometimes');
|
||||||
|
}
|
||||||
|
|
||||||
// this function is a promise for error stack tracking purposes.
|
// this function is a promise for error stack tracking purposes.
|
||||||
// this function expects the last argument of args to be a callback(err, serverData)
|
// this function expects the last argument of args to be a callback(err, serverData)
|
||||||
static socketEmitTimeout(
|
static socketEmitTimeout(
|
||||||
|
Loading…
Reference in New Issue
Block a user