2021-12-13 04:01:30 +00:00
|
|
|
import * as electronRemote from '@electron/remote';
|
|
|
|
const electronConsole = electronRemote.getGlobal('console') as Console;
|
|
|
|
import Logger from '../../../../logger/logger';
|
|
|
|
const LOG = Logger.create(__filename, electronConsole);
|
|
|
|
|
2021-12-24 23:37:27 +00:00
|
|
|
import { Changes, GuildMetadata, Member, Message, Resource } from "../../data-types";
|
2021-12-13 04:01:30 +00:00
|
|
|
import CombinedGuild from "../../guild-combined";
|
|
|
|
|
2021-12-24 23:37:27 +00:00
|
|
|
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
|
2021-12-13 04:01:30 +00:00
|
|
|
import { AutoVerifierChangesType } from "../../auto-verifier";
|
|
|
|
import { Conflictable, Connectable } from "../../guild-types";
|
|
|
|
import { EventEmitter } from 'tsee';
|
2021-12-24 23:37:27 +00:00
|
|
|
import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args';
|
2021-12-17 01:25:55 +00:00
|
|
|
import { Token, Channel } from '../../data-types';
|
2021-12-31 03:28:30 +00:00
|
|
|
import { useIsMountedRef, useOneTimeAsyncAction } from './react-helper';
|
2021-12-24 23:37:27 +00:00
|
|
|
import Globals from '../../globals';
|
2021-12-25 18:00:20 +00:00
|
|
|
import ElementsUtil from './elements-util';
|
2021-12-13 04:01:30 +00:00
|
|
|
|
2021-12-13 05:25:23 +00:00
|
|
|
export type SingleSubscriptionEvents = {
|
2021-12-13 04:01:30 +00:00
|
|
|
'fetch': () => void;
|
2021-12-13 05:25:23 +00:00
|
|
|
'updated': () => void;
|
2021-12-13 04:01:30 +00:00
|
|
|
'conflict': () => void;
|
|
|
|
'fetch-error': () => void;
|
|
|
|
}
|
|
|
|
|
2021-12-13 05:25:23 +00:00
|
|
|
export type MultipleSubscriptionEvents<T> = {
|
|
|
|
'fetch': () => void;
|
|
|
|
'fetch-error': () => void;
|
|
|
|
'new': (newValues: T[]) => void;
|
|
|
|
'updated': (updatedValues: T[]) => void;
|
|
|
|
'removed': (removedValues: T[]) => void;
|
|
|
|
'conflict': (changes: Changes<T>) => void;
|
|
|
|
}
|
|
|
|
|
2021-12-13 04:01:30 +00:00
|
|
|
interface EffectParams<T> {
|
|
|
|
guild: CombinedGuild;
|
|
|
|
onFetch: (value: T | null) => void;
|
|
|
|
onFetchError: (e: unknown) => void;
|
2021-12-13 04:34:29 +00:00
|
|
|
bindEventsFunc: () => void;
|
|
|
|
unbindEventsFunc: () => void;
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
|
|
|
|
|
2021-12-13 05:25:23 +00:00
|
|
|
interface SingleEventMappingParams<T, UE extends keyof Connectable, CE extends keyof Conflictable> {
|
|
|
|
updatedEventName: UE;
|
|
|
|
updatedEventArgsMap: (...args: Arguments<Connectable[UE]>) => T;
|
2021-12-13 04:01:30 +00:00
|
|
|
conflictEventName: CE;
|
2021-12-13 05:25:23 +00:00
|
|
|
conflictEventArgsMap: (...args: Arguments<Conflictable[CE]>) => T;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MultipleEventMappingParams<
|
|
|
|
T,
|
|
|
|
NE extends keyof Connectable,
|
|
|
|
UE extends keyof Connectable,
|
|
|
|
RE extends keyof Connectable,
|
|
|
|
CE extends keyof Conflictable
|
|
|
|
> {
|
|
|
|
newEventName: NE;
|
|
|
|
newEventArgsMap: (...args: Arguments<Connectable[NE]>) => T[]; // list of new elements
|
|
|
|
updatedEventName: UE;
|
|
|
|
updatedEventArgsMap: (...args: Arguments<Connectable[UE]>) => T[]; // list of updated elements
|
|
|
|
removedEventName: RE;
|
|
|
|
removedEventArgsMap: (...args: Arguments<Connectable[RE]>) => T[]; // list of removed elements
|
|
|
|
|
|
|
|
conflictEventName: CE;
|
|
|
|
conflictEventArgsMap: (...args: Arguments<Conflictable[CE]>) => Changes<T>;
|
|
|
|
|
2021-12-24 23:37:27 +00:00
|
|
|
sortFunc: (a: T, b: T) => number; // Friendly reminder that v8 uses timsort so this is O(n) for pre-sorted stuff
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default class GuildSubscriptions {
|
2021-12-13 04:34:29 +00:00
|
|
|
private static useGuildSubscriptionEffect<T>(
|
|
|
|
subscriptionParams: EffectParams<T>,
|
2021-12-13 07:32:29 +00:00
|
|
|
fetchFunc: (() => Promise<T>) | (() => Promise<T | null>)
|
2021-12-24 23:37:27 +00:00
|
|
|
): [ fetchRetryCallable: () => Promise<void> ] {
|
2021-12-13 04:34:29 +00:00
|
|
|
const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams;
|
2021-12-13 04:01:30 +00:00
|
|
|
|
2021-12-31 03:28:30 +00:00
|
|
|
const isMounted = useIsMountedRef();
|
2021-12-24 23:37:27 +00:00
|
|
|
|
|
|
|
const fetchManagerFunc = useCallback(async () => {
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
try {
|
|
|
|
const value = await fetchFunc();
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
onFetch(value);
|
|
|
|
} catch (e: unknown) {
|
|
|
|
LOG.error('error fetching for subscription', e);
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
onFetchError(e);
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
2021-12-13 07:32:29 +00:00
|
|
|
}, [ fetchFunc ]);
|
2021-12-13 04:01:30 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// Bind guild events to make sure we have the most up to date information
|
|
|
|
guild.on('connect', fetchManagerFunc);
|
2021-12-13 04:34:29 +00:00
|
|
|
bindEventsFunc();
|
2021-12-13 04:01:30 +00:00
|
|
|
|
|
|
|
// Fetch the data once
|
|
|
|
fetchManagerFunc();
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
// Unbind the events so that we don't have any memory leaks
|
|
|
|
guild.off('connect', fetchManagerFunc);
|
2021-12-13 04:34:29 +00:00
|
|
|
unbindEventsFunc();
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
|
|
|
}, [ fetchManagerFunc ]);
|
2021-12-24 23:37:27 +00:00
|
|
|
|
|
|
|
return [ fetchManagerFunc ];
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
|
|
|
|
2021-12-13 04:34:29 +00:00
|
|
|
private static useSingleGuildSubscription<T, UE extends keyof Connectable, CE extends keyof Conflictable>(
|
2021-12-13 05:25:23 +00:00
|
|
|
guild: CombinedGuild,
|
|
|
|
eventMappingParams: SingleEventMappingParams<T, UE, CE>,
|
2021-12-13 07:32:29 +00:00
|
|
|
fetchFunc: (() => Promise<T>) | (() => Promise<T | null>)
|
2021-12-13 05:25:23 +00:00
|
|
|
): [value: T | null, fetchError: unknown | null, events: EventEmitter<SingleSubscriptionEvents>] {
|
|
|
|
const { updatedEventName, updatedEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams;
|
2021-12-13 04:34:29 +00:00
|
|
|
|
2021-12-31 03:28:30 +00:00
|
|
|
const isMounted = useIsMountedRef();
|
2021-12-13 04:34:29 +00:00
|
|
|
|
2021-12-13 04:01:30 +00:00
|
|
|
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
|
|
|
|
const [ value, setValue ] = useState<T | null>(null);
|
|
|
|
|
2021-12-13 05:25:23 +00:00
|
|
|
const events = useMemo(() => new EventEmitter<SingleSubscriptionEvents>(), []);
|
2021-12-13 04:01:30 +00:00
|
|
|
|
|
|
|
const onFetch = useCallback((fetchValue: T | null) => {
|
|
|
|
setValue(fetchValue);
|
|
|
|
setFetchError(null);
|
|
|
|
events.emit('fetch');
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const onFetchError = useCallback((e: unknown) => {
|
|
|
|
setFetchError(e);
|
|
|
|
setValue(null);
|
|
|
|
events.emit('fetch-error');
|
|
|
|
}, []);
|
|
|
|
|
2021-12-13 05:25:23 +00:00
|
|
|
const onUpdated = useCallback((updateValue: T) => {
|
2021-12-13 04:01:30 +00:00
|
|
|
setValue(updateValue);
|
2021-12-13 05:25:23 +00:00
|
|
|
events.emit('updated');
|
2021-12-13 04:01:30 +00:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
const onConflict = useCallback((conflictValue: T) => {
|
|
|
|
setValue(conflictValue);
|
|
|
|
events.emit('conflict');
|
|
|
|
}, []);
|
|
|
|
|
2021-12-13 04:34:29 +00:00
|
|
|
// 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
|
|
|
|
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
|
2021-12-24 23:37:27 +00:00
|
|
|
if (!isMounted.current) return;
|
2021-12-13 05:25:23 +00:00
|
|
|
const value = updatedEventArgsMap(...args);
|
|
|
|
onUpdated(value);
|
2021-12-13 04:34:29 +00:00
|
|
|
}, []) as (Connectable & Conflictable)[UE];
|
|
|
|
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
|
2021-12-24 23:37:27 +00:00
|
|
|
if (!isMounted.current) return;
|
2021-12-13 04:34:29 +00:00
|
|
|
const value = conflictEventArgsMap(...args);
|
|
|
|
onConflict(value);
|
|
|
|
}, []) as (Connectable & Conflictable)[CE];
|
|
|
|
|
|
|
|
const bindEventsFunc = useCallback(() => {
|
2021-12-13 05:25:23 +00:00
|
|
|
guild.on(updatedEventName, boundUpdateFunc);
|
|
|
|
guild.on(conflictEventName, boundConflictFunc);
|
|
|
|
}, []);
|
|
|
|
const unbindEventsFunc = useCallback(() => {
|
|
|
|
guild.off(updatedEventName, boundUpdateFunc);
|
|
|
|
guild.off(conflictEventName, boundConflictFunc);
|
|
|
|
}, []);
|
|
|
|
|
2021-12-24 23:37:27 +00:00
|
|
|
GuildSubscriptions.useGuildSubscriptionEffect({
|
2021-12-13 05:25:23 +00:00
|
|
|
guild,
|
|
|
|
onFetch,
|
|
|
|
onFetchError,
|
|
|
|
bindEventsFunc,
|
|
|
|
unbindEventsFunc
|
2021-12-13 07:32:29 +00:00
|
|
|
}, fetchFunc);
|
2021-12-13 05:25:23 +00:00
|
|
|
|
|
|
|
return [ value, fetchError, events ];
|
|
|
|
}
|
|
|
|
|
|
|
|
private static useMultipleGuildSubscription<
|
|
|
|
T extends { id: string },
|
|
|
|
NE extends keyof Connectable,
|
|
|
|
UE extends keyof Connectable,
|
|
|
|
RE extends keyof Connectable,
|
|
|
|
CE extends keyof Conflictable
|
|
|
|
>(
|
|
|
|
guild: CombinedGuild,
|
|
|
|
eventMappingParams: MultipleEventMappingParams<T, NE, UE, RE, CE>,
|
2021-12-13 07:32:29 +00:00
|
|
|
fetchFunc: (() => Promise<T[]>) | (() => Promise<T[] | null>)
|
2021-12-24 23:37:27 +00:00
|
|
|
): [
|
|
|
|
fetchRetryCallable: () => Promise<void>,
|
|
|
|
value: T[] | null,
|
|
|
|
fetchError: unknown | null,
|
|
|
|
events: EventEmitter<MultipleSubscriptionEvents<T>>
|
|
|
|
] {
|
2021-12-13 05:25:23 +00:00
|
|
|
const {
|
|
|
|
newEventName, newEventArgsMap,
|
|
|
|
updatedEventName, updatedEventArgsMap,
|
|
|
|
removedEventName, removedEventArgsMap,
|
|
|
|
conflictEventName, conflictEventArgsMap,
|
|
|
|
sortFunc
|
|
|
|
} = eventMappingParams;
|
|
|
|
|
2021-12-31 03:28:30 +00:00
|
|
|
const isMounted = useIsMountedRef();
|
2021-12-13 05:25:23 +00:00
|
|
|
|
|
|
|
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
|
|
|
|
const [ value, setValue ] = useState<T[] | null>(null);
|
|
|
|
|
|
|
|
const events = useMemo(() => new EventEmitter<MultipleSubscriptionEvents<T>>(), []);
|
|
|
|
|
|
|
|
const onFetch = useCallback((fetchValue: T[] | null) => {
|
2021-12-13 08:29:06 +00:00
|
|
|
if (fetchValue) fetchValue.sort(sortFunc);
|
2021-12-13 05:25:23 +00:00
|
|
|
setValue(fetchValue);
|
|
|
|
setFetchError(null);
|
|
|
|
events.emit('fetch');
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ sortFunc ]);
|
2021-12-13 05:25:23 +00:00
|
|
|
|
|
|
|
const onFetchError = useCallback((e: unknown) => {
|
|
|
|
setFetchError(e);
|
|
|
|
setValue(null);
|
|
|
|
events.emit('fetch-error');
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const onNew = useCallback((newElements: T[]) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
return currentValue.concat(newElements).sort(sortFunc);
|
|
|
|
})
|
|
|
|
events.emit('new', newElements);
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ sortFunc ]);
|
2021-12-13 05:25:23 +00:00
|
|
|
const onUpdated = useCallback((updatedElements: T[]) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
|
|
|
|
});
|
|
|
|
events.emit('updated', updatedElements);
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ sortFunc ]);
|
2021-12-13 05:25:23 +00:00
|
|
|
const onRemoved = useCallback((removedElements: T[]) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id));
|
|
|
|
return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc);
|
|
|
|
});
|
|
|
|
events.emit('removed', removedElements);
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ sortFunc ]);
|
2021-12-13 05:25:23 +00:00
|
|
|
|
|
|
|
const onConflict = useCallback((changes: Changes<T>) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id));
|
|
|
|
return currentValue
|
|
|
|
.concat(changes.added)
|
|
|
|
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element)
|
|
|
|
.filter(element => !deletedIds.has(element.id))
|
|
|
|
.sort(sortFunc);
|
|
|
|
});
|
|
|
|
events.emit('conflict', changes);
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ sortFunc ]);
|
2021-12-13 05:25:23 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
const boundNewFunc = useCallback((...args: Arguments<Connectable[NE]>): void => {
|
2021-12-24 23:37:27 +00:00
|
|
|
if (!isMounted.current) return;
|
2021-12-13 05:25:23 +00:00
|
|
|
onNew(newEventArgsMap(...args));
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ onNew, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
|
2021-12-13 05:25:23 +00:00
|
|
|
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
|
2021-12-24 23:37:27 +00:00
|
|
|
if (!isMounted.current) return;
|
2021-12-13 05:25:23 +00:00
|
|
|
onUpdated(updatedEventArgsMap(...args));
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ onUpdated, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
|
2021-12-13 05:25:23 +00:00
|
|
|
const boundRemovedFunc = useCallback((...args: Arguments<Connectable[RE]>): void => {
|
2021-12-24 23:37:27 +00:00
|
|
|
if (!isMounted.current) return;
|
2021-12-13 05:25:23 +00:00
|
|
|
onRemoved(removedEventArgsMap(...args));
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ onRemoved, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
|
2021-12-13 05:25:23 +00:00
|
|
|
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
|
2021-12-24 23:37:27 +00:00
|
|
|
if (!isMounted.current) return;
|
2021-12-13 05:25:23 +00:00
|
|
|
onConflict(conflictEventArgsMap(...args));
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ onConflict, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
|
2021-12-13 05:25:23 +00:00
|
|
|
|
|
|
|
const bindEventsFunc = useCallback(() => {
|
|
|
|
guild.on(newEventName, boundNewFunc);
|
|
|
|
guild.on(updatedEventName, boundUpdateFunc);
|
|
|
|
guild.on(removedEventName, boundRemovedFunc);
|
2021-12-13 04:34:29 +00:00
|
|
|
guild.on(conflictEventName, boundConflictFunc);
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]);
|
2021-12-13 04:34:29 +00:00
|
|
|
const unbindEventsFunc = useCallback(() => {
|
2021-12-13 05:25:23 +00:00
|
|
|
guild.off(newEventName, boundNewFunc);
|
|
|
|
guild.off(updatedEventName, boundUpdateFunc);
|
|
|
|
guild.off(removedEventName, boundRemovedFunc);
|
2021-12-13 04:34:29 +00:00
|
|
|
guild.off(conflictEventName, boundConflictFunc);
|
2021-12-24 23:37:27 +00:00
|
|
|
}, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]);
|
|
|
|
|
|
|
|
const [ fetchRetryCallable ] = GuildSubscriptions.useGuildSubscriptionEffect({
|
|
|
|
guild,
|
|
|
|
onFetch,
|
|
|
|
onFetchError,
|
|
|
|
bindEventsFunc,
|
|
|
|
unbindEventsFunc
|
|
|
|
}, fetchFunc);
|
|
|
|
|
|
|
|
return [ fetchRetryCallable, value, fetchError, events ];
|
|
|
|
}
|
|
|
|
|
|
|
|
private static useMultipleGuildSubscriptionScrolling<
|
|
|
|
T extends { id: string },
|
|
|
|
NE extends keyof Connectable,
|
|
|
|
UE extends keyof Connectable,
|
|
|
|
RE extends keyof Connectable,
|
|
|
|
CE extends keyof Conflictable
|
|
|
|
>(
|
|
|
|
guild: CombinedGuild,
|
|
|
|
eventMappingParams: MultipleEventMappingParams<T, NE, UE, RE, CE>,
|
|
|
|
maxElements: number,
|
|
|
|
maxFetchElements: number,
|
|
|
|
fetchFunc: (() => Promise<T[]>) | (() => Promise<T[] | null>),
|
|
|
|
fetchAboveFunc: ((reference: T) => Promise<T[] | null>),
|
|
|
|
fetchBelowFunc: ((reference: T) => Promise<T[] | null>),
|
|
|
|
): [
|
|
|
|
fetchRetryCallable: () => Promise<void>,
|
|
|
|
fetchAboveCallable: () => Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }>,
|
|
|
|
fetchBelowCallable: () => Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }>,
|
|
|
|
setScrollRatio: Dispatch<SetStateAction<number>>,
|
|
|
|
fetchResult: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null,
|
|
|
|
value: T[] | null,
|
|
|
|
fetchError: unknown | null,
|
|
|
|
fetchAboveError: unknown | null,
|
|
|
|
fetchBelowError: unknown | null,
|
|
|
|
events: EventEmitter<MultipleSubscriptionEvents<T>>
|
|
|
|
] {
|
|
|
|
const {
|
|
|
|
newEventName, newEventArgsMap,
|
|
|
|
updatedEventName, updatedEventArgsMap,
|
|
|
|
removedEventName, removedEventArgsMap,
|
|
|
|
conflictEventName, conflictEventArgsMap,
|
|
|
|
sortFunc
|
|
|
|
} = eventMappingParams;
|
|
|
|
|
2021-12-31 03:28:30 +00:00
|
|
|
const isMounted = useIsMountedRef();
|
2021-12-24 23:37:27 +00:00
|
|
|
|
|
|
|
const [ value, setValue ] = useState<T[] | null>(null);
|
|
|
|
|
|
|
|
const [ fetchError, setFetchError ] = useState<unknown | null>(null);
|
|
|
|
const [ fetchAboveError, setFetchAboveError ] = useState<unknown | null>(null);
|
|
|
|
const [ fetchBelowError, setFetchBelowError ] = useState<unknown | null>(null);
|
|
|
|
|
|
|
|
const [ fetchResult, setFetchResult ] = useState<{ hasMoreAbove: boolean, hasMoreBelow: boolean } | null>(null);
|
|
|
|
|
|
|
|
// Percentage of scroll from top. i.e. 300px from top of 1000px scroll = 0.3 scroll ratio
|
|
|
|
const [ scrollRatio, setScrollRatio ] = useState<number>(0.5);
|
|
|
|
|
|
|
|
// Gets the number of elements to remove from the top or bottom. Tries to optimise so that if 10 elements
|
|
|
|
// are removed and we're 30% scrolled down, 3 get removed from the top and 7 are removed from the bottom.
|
|
|
|
// TBH, this function is pretty overkill but that's important
|
|
|
|
const getRemoveCounts = useCallback((toRemove: number): { fromTop: number, fromBottom: number } => {
|
|
|
|
// Make sure we round toward the side of the scrollbar that we are not closest to
|
|
|
|
// this is important for the most common case of 1 removed element.
|
|
|
|
const topRoundFunc = scrollRatio > 0.5 ? Math.ceil : Math.floor;
|
|
|
|
const bottomRoundFunc = scrollRatio > 0.5 ? Math.floor : Math.ceil;
|
|
|
|
return {
|
|
|
|
fromTop: topRoundFunc(toRemove * scrollRatio),
|
|
|
|
fromBottom: bottomRoundFunc(toRemove * scrollRatio)
|
|
|
|
}
|
|
|
|
}, [ scrollRatio ]);
|
|
|
|
|
|
|
|
function removeByCounts<T>(elements: T[], counts: { fromTop: number, fromBottom: number }): T[] {
|
|
|
|
const { fromTop, fromBottom } = counts;
|
|
|
|
return elements.slice(fromTop, elements.length - fromBottom);
|
|
|
|
}
|
|
|
|
|
|
|
|
const events = useMemo(() => new EventEmitter<MultipleSubscriptionEvents<T>>(), []);
|
|
|
|
|
|
|
|
const fetchAboveCallable = useCallback(async (): Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }> => {
|
|
|
|
if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false };
|
|
|
|
if (!value || value.length === 0) return { hasMoreAbove: false, removedFromBottom: false };
|
|
|
|
try {
|
|
|
|
const reference = value[0] as T;
|
|
|
|
const aboveElements = await fetchAboveFunc(reference);
|
|
|
|
if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false };
|
|
|
|
setFetchAboveError(null);
|
|
|
|
if (aboveElements) {
|
|
|
|
const hasMoreAbove = aboveElements.length >= maxFetchElements;
|
|
|
|
let removedFromBottom = false;
|
|
|
|
setValue(currentValue => {
|
|
|
|
let newValue = aboveElements.concat(currentValue ?? []).sort(sortFunc);
|
|
|
|
if (newValue.length > maxElements) {
|
|
|
|
newValue = newValue.slice(0, maxElements);
|
|
|
|
removedFromBottom = true;
|
|
|
|
}
|
|
|
|
return newValue;
|
|
|
|
});
|
|
|
|
return { hasMoreAbove, removedFromBottom };
|
|
|
|
} else {
|
|
|
|
return { hasMoreAbove: false, removedFromBottom: false };
|
|
|
|
}
|
|
|
|
} catch (e: unknown) {
|
|
|
|
LOG.error('error fetching above for subscription', e);
|
|
|
|
if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false };
|
|
|
|
setFetchAboveError(e);
|
|
|
|
return { hasMoreAbove: true, removedFromBottom: false };
|
|
|
|
}
|
|
|
|
}, [ value, fetchAboveFunc, maxFetchElements ]);
|
|
|
|
|
|
|
|
const fetchBelowCallable = useCallback(async (): Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }> => {
|
|
|
|
if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false };
|
|
|
|
if (!value || value.length === 0) return { hasMoreBelow: false, removedFromTop: false };
|
|
|
|
try {
|
|
|
|
const reference = value[value.length - 1] as T;
|
|
|
|
const belowElements = await fetchBelowFunc(reference);
|
|
|
|
if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false };
|
|
|
|
setFetchBelowError(null);
|
|
|
|
if (belowElements) {
|
|
|
|
const hasMoreBelow = belowElements.length >= maxFetchElements;
|
|
|
|
let removedFromTop = false;
|
|
|
|
setValue(currentValue => {
|
|
|
|
let newValue = (currentValue ?? []).concat(belowElements).sort(sortFunc);
|
|
|
|
if (newValue.length > maxElements) {
|
|
|
|
newValue = newValue.slice(Math.max(newValue.length - maxElements, 0));
|
|
|
|
removedFromTop = true;
|
|
|
|
}
|
|
|
|
return newValue;
|
|
|
|
});
|
|
|
|
return { hasMoreBelow, removedFromTop };
|
|
|
|
} else {
|
|
|
|
return { hasMoreBelow: false, removedFromTop: false };
|
|
|
|
}
|
|
|
|
} catch (e: unknown) {
|
|
|
|
LOG.error('error fetching below for subscription', e);
|
|
|
|
if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false };
|
|
|
|
setFetchBelowError(e);
|
|
|
|
return { hasMoreBelow: true, removedFromTop: false };
|
|
|
|
}
|
|
|
|
}, [ value, fetchBelowFunc, maxFetchElements ]);
|
|
|
|
|
|
|
|
const onFetch = useCallback((fetchValue: T[] | null) => {
|
|
|
|
let hasMoreAbove = false;
|
|
|
|
if (fetchValue) {
|
|
|
|
if (fetchValue.length >= maxFetchElements) hasMoreAbove = true;
|
|
|
|
fetchValue = fetchValue.slice(Math.max(fetchValue.length - maxElements)).sort(sortFunc);
|
|
|
|
}
|
|
|
|
setFetchResult({ hasMoreAbove, hasMoreBelow: false });
|
|
|
|
setValue(fetchValue);
|
|
|
|
setFetchError(null);
|
|
|
|
events.emit('fetch');
|
|
|
|
}, [ sortFunc, maxFetchElements, maxElements ]);
|
|
|
|
|
|
|
|
const onFetchError = useCallback((e: unknown) => {
|
|
|
|
setFetchError(e);
|
|
|
|
setValue(null);
|
|
|
|
events.emit('fetch-error');
|
2021-12-13 04:34:29 +00:00
|
|
|
}, []);
|
|
|
|
|
2021-12-24 23:37:27 +00:00
|
|
|
const onNew = useCallback((newElements: T[]) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
let newValue = currentValue.concat(newElements).sort(sortFunc);
|
|
|
|
if (newValue.length > maxElements) {
|
|
|
|
newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements));
|
|
|
|
}
|
|
|
|
return newValue;
|
|
|
|
})
|
|
|
|
events.emit('new', newElements);
|
|
|
|
}, [ sortFunc, getRemoveCounts ]);
|
|
|
|
const onUpdated = useCallback((updatedElements: T[]) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
|
|
|
|
});
|
|
|
|
events.emit('updated', updatedElements);
|
|
|
|
}, [ sortFunc ]);
|
|
|
|
const onRemoved = useCallback((removedElements: T[]) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id));
|
|
|
|
return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc);
|
|
|
|
});
|
|
|
|
events.emit('removed', removedElements);
|
|
|
|
}, [ sortFunc ]);
|
|
|
|
|
|
|
|
const onConflict = useCallback((changes: Changes<T>) => {
|
|
|
|
setValue(currentValue => {
|
|
|
|
if (currentValue === null) return null;
|
|
|
|
const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id));
|
|
|
|
let newValue = currentValue
|
|
|
|
.concat(changes.added)
|
|
|
|
.map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element)
|
|
|
|
.filter(element => !deletedIds.has(element.id))
|
|
|
|
.sort(sortFunc);
|
|
|
|
if (newValue.length > maxElements) {
|
|
|
|
newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements));
|
|
|
|
}
|
|
|
|
return newValue;
|
|
|
|
});
|
|
|
|
events.emit('conflict', changes);
|
|
|
|
}, [ sortFunc, getRemoveCounts ]);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
const boundNewFunc = useCallback((...args: Arguments<Connectable[NE]>): void => {
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
onNew(newEventArgsMap(...args));
|
|
|
|
}, [ onNew, newEventArgsMap ]) as (Connectable & Conflictable)[NE];
|
|
|
|
const boundUpdateFunc = useCallback((...args: Arguments<Connectable[UE]>): void => {
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
onUpdated(updatedEventArgsMap(...args));
|
|
|
|
}, [ onUpdated, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE];
|
|
|
|
const boundRemovedFunc = useCallback((...args: Arguments<Connectable[RE]>): void => {
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
onRemoved(removedEventArgsMap(...args));
|
|
|
|
}, [ onRemoved, removedEventArgsMap ]) as (Connectable & Conflictable)[RE];
|
|
|
|
const boundConflictFunc = useCallback((...args: Arguments<Conflictable[CE]>): void => {
|
|
|
|
if (!isMounted.current) return;
|
|
|
|
onConflict(conflictEventArgsMap(...args));
|
|
|
|
}, [ onConflict, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE];
|
|
|
|
|
|
|
|
const bindEventsFunc = useCallback(() => {
|
|
|
|
guild.on(newEventName, boundNewFunc);
|
|
|
|
guild.on(updatedEventName, boundUpdateFunc);
|
|
|
|
guild.on(removedEventName, boundRemovedFunc);
|
|
|
|
guild.on(conflictEventName, boundConflictFunc);
|
|
|
|
}, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]);
|
|
|
|
const unbindEventsFunc = useCallback(() => {
|
|
|
|
guild.off(newEventName, boundNewFunc);
|
|
|
|
guild.off(updatedEventName, boundUpdateFunc);
|
|
|
|
guild.off(removedEventName, boundRemovedFunc);
|
|
|
|
guild.off(conflictEventName, boundConflictFunc);
|
|
|
|
}, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]);
|
|
|
|
|
|
|
|
const [ fetchRetryCallable ] = GuildSubscriptions.useGuildSubscriptionEffect({
|
2021-12-13 04:01:30 +00:00
|
|
|
guild,
|
|
|
|
onFetch,
|
2021-12-13 04:34:29 +00:00
|
|
|
onFetchError,
|
|
|
|
bindEventsFunc,
|
|
|
|
unbindEventsFunc
|
2021-12-13 07:32:29 +00:00
|
|
|
}, fetchFunc);
|
2021-12-13 04:01:30 +00:00
|
|
|
|
2021-12-24 23:37:27 +00:00
|
|
|
return [
|
|
|
|
fetchRetryCallable,
|
|
|
|
fetchAboveCallable,
|
|
|
|
fetchBelowCallable,
|
|
|
|
setScrollRatio,
|
|
|
|
fetchResult,
|
|
|
|
value,
|
|
|
|
fetchError,
|
|
|
|
fetchAboveError,
|
|
|
|
fetchBelowError,
|
|
|
|
events
|
|
|
|
];
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static useGuildMetadataSubscription(guild: CombinedGuild) {
|
2021-12-13 07:32:29 +00:00
|
|
|
const fetchMetadataFunc = useCallback(async () => {
|
|
|
|
//LOG.silly('fetching metadata for subscription');
|
|
|
|
return await guild.fetchMetadata();
|
|
|
|
}, [ guild ]);
|
2021-12-13 04:34:29 +00:00
|
|
|
return GuildSubscriptions.useSingleGuildSubscription<GuildMetadata, 'update-metadata', 'conflict-metadata'>(guild, {
|
2021-12-13 05:25:23 +00:00
|
|
|
updatedEventName: 'update-metadata',
|
|
|
|
updatedEventArgsMap: (guildMeta: GuildMetadata) => guildMeta,
|
2021-12-13 04:01:30 +00:00
|
|
|
conflictEventName: 'conflict-metadata',
|
2021-12-13 04:34:29 +00:00
|
|
|
conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta
|
2021-12-13 07:32:29 +00:00
|
|
|
}, fetchMetadataFunc);
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static useResourceSubscription(guild: CombinedGuild, resourceId: string | null) {
|
2021-12-13 07:32:29 +00:00
|
|
|
const fetchResourceFunc = useCallback(async () => {
|
|
|
|
//LOG.silly('fetching resource for subscription (resourceId: ' + resourceId + ')');
|
|
|
|
if (resourceId === null) return null;
|
|
|
|
return await guild.fetchResource(resourceId);
|
|
|
|
}, [ guild, resourceId ]);
|
2021-12-13 04:34:29 +00:00
|
|
|
return GuildSubscriptions.useSingleGuildSubscription<Resource, 'update-resource', 'conflict-resource'>(guild, {
|
2021-12-13 05:25:23 +00:00
|
|
|
updatedEventName: 'update-resource',
|
|
|
|
updatedEventArgsMap: (resource: Resource) => resource,
|
2021-12-13 04:01:30 +00:00
|
|
|
conflictEventName: 'conflict-resource',
|
2021-12-13 04:34:29 +00:00
|
|
|
conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource
|
2021-12-13 07:32:29 +00:00
|
|
|
}, fetchResourceFunc);
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|
2021-12-13 05:25:23 +00:00
|
|
|
|
2021-12-26 01:21:15 +00:00
|
|
|
static useSoftImageSrcResourceSubscription(guild: CombinedGuild, resourceId: string | null): [ imgSrc: string, resource: Resource | null, fetchError: unknown | null ] {
|
|
|
|
const [ resource, fetchError ] = GuildSubscriptions.useResourceSubscription(guild, resourceId);
|
2021-12-25 18:00:20 +00:00
|
|
|
|
2021-12-31 03:28:30 +00:00
|
|
|
const [ imgSrc ] = useOneTimeAsyncAction(
|
2021-12-25 18:00:20 +00:00
|
|
|
async () => {
|
|
|
|
if (fetchError) return './img/error.png';
|
2021-12-26 01:21:15 +00:00
|
|
|
if (!resource) return './img/loading.svg';
|
|
|
|
return await ElementsUtil.getImageSrcFromBufferFailSoftly(resource.data);
|
2021-12-25 18:00:20 +00:00
|
|
|
},
|
|
|
|
'./img/loading.svg',
|
2021-12-26 01:21:15 +00:00
|
|
|
[ resource, fetchError ]
|
2021-12-25 18:00:20 +00:00
|
|
|
);
|
|
|
|
|
2021-12-26 01:21:15 +00:00
|
|
|
return [ imgSrc, resource, fetchError ];
|
2021-12-25 18:00:20 +00:00
|
|
|
}
|
|
|
|
|
2021-12-17 01:25:55 +00:00
|
|
|
static useChannelsSubscription(guild: CombinedGuild) {
|
|
|
|
const fetchChannelsFunc = useCallback(async () => {
|
|
|
|
return await guild.fetchChannels();
|
|
|
|
}, [ guild ]);
|
|
|
|
return GuildSubscriptions.useMultipleGuildSubscription<Channel, 'new-channels', 'update-channels', 'remove-channels', 'conflict-channels'>(guild, {
|
|
|
|
newEventName: 'new-channels',
|
|
|
|
newEventArgsMap: (channels: Channel[]) => channels,
|
|
|
|
updatedEventName: 'update-channels',
|
|
|
|
updatedEventArgsMap: (updatedChannels: Channel[]) => updatedChannels,
|
|
|
|
removedEventName: 'remove-channels',
|
|
|
|
removedEventArgsMap: (removedChannels: Channel[]) => removedChannels,
|
|
|
|
conflictEventName: 'conflict-channels',
|
|
|
|
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Channel>) => changes,
|
|
|
|
sortFunc: Channel.sortByIndex
|
|
|
|
}, fetchChannelsFunc);
|
|
|
|
}
|
|
|
|
|
2021-12-21 05:24:05 +00:00
|
|
|
static useMembersSubscription(guild: CombinedGuild) {
|
|
|
|
const fetchMembersFunc = useCallback(async () => {
|
|
|
|
return await guild.fetchMembers();
|
|
|
|
}, [ guild ]);
|
|
|
|
return GuildSubscriptions.useMultipleGuildSubscription<Member, 'new-members', 'update-members', 'remove-members', 'conflict-members'>(guild, {
|
|
|
|
newEventName: 'new-members',
|
|
|
|
newEventArgsMap: (members: Member[]) => members,
|
|
|
|
updatedEventName: 'update-members',
|
|
|
|
updatedEventArgsMap: (updatedMembers: Member[]) => updatedMembers,
|
|
|
|
removedEventName: 'remove-members',
|
|
|
|
removedEventArgsMap: (removedMembers: Member[]) => removedMembers,
|
|
|
|
conflictEventName: 'conflict-members',
|
|
|
|
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Member>) => changes,
|
|
|
|
sortFunc: Member.sortForList
|
|
|
|
}, fetchMembersFunc);
|
|
|
|
}
|
|
|
|
|
2021-12-25 18:00:20 +00:00
|
|
|
static useSelfMemberSubscription(guild: CombinedGuild): [ selfMember: Member | null ] {
|
|
|
|
const [ fetchRetryCallable, members, fetchError ] = GuildSubscriptions.useMembersSubscription(guild);
|
|
|
|
|
|
|
|
// TODO: Show an error if we can't fetch and allow retry
|
|
|
|
|
|
|
|
const selfMember = useMemo(() => {
|
|
|
|
if (members) {
|
|
|
|
const member = members.find(m => m.id === guild.memberId);
|
|
|
|
if (!member) {
|
|
|
|
LOG.warn('Unable to find self in members');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return member;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}, [ guild.memberId, members ]);
|
|
|
|
|
|
|
|
return [ selfMember ];
|
|
|
|
}
|
|
|
|
|
2021-12-13 05:25:23 +00:00
|
|
|
static useTokensSubscription(guild: CombinedGuild) {
|
2021-12-13 07:32:29 +00:00
|
|
|
const fetchTokensFunc = useCallback(async () => {
|
|
|
|
//LOG.silly('fetching tokens for subscription');
|
|
|
|
return await guild.fetchTokens();
|
|
|
|
}, [ guild ]);
|
2021-12-13 05:25:23 +00:00
|
|
|
return GuildSubscriptions.useMultipleGuildSubscription<Token, 'new-tokens', 'update-tokens', 'remove-tokens', 'conflict-tokens'>(guild, {
|
|
|
|
newEventName: 'new-tokens',
|
|
|
|
newEventArgsMap: (tokens: Token[]) => tokens,
|
|
|
|
updatedEventName: 'update-tokens',
|
|
|
|
updatedEventArgsMap: (updatedTokens: Token[]) => updatedTokens,
|
|
|
|
removedEventName: 'remove-tokens',
|
|
|
|
removedEventArgsMap: (removedTokens: Token[]) => removedTokens,
|
|
|
|
conflictEventName: 'conflict-tokens',
|
|
|
|
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Token>) => changes,
|
2021-12-13 08:39:40 +00:00
|
|
|
sortFunc: Token.sortRecentCreatedFirst
|
2021-12-13 07:32:29 +00:00
|
|
|
}, fetchTokensFunc);
|
2021-12-13 05:25:23 +00:00
|
|
|
}
|
2021-12-24 23:37:27 +00:00
|
|
|
|
|
|
|
static useMessagesScrollingSubscription(guild: CombinedGuild, channel: Channel) {
|
|
|
|
const maxFetchElements = Globals.MESSAGES_PER_REQUEST;
|
|
|
|
const maxElements = Globals.MAX_CURRENT_MESSAGES;
|
|
|
|
const fetchMessagesFunc = useCallback(async () => {
|
|
|
|
return await guild.fetchMessagesRecent(channel.id, maxFetchElements);
|
|
|
|
}, [ guild, channel.id, maxFetchElements ]);
|
|
|
|
const fetchAboveFunc = useCallback(async (reference: Message) => {
|
|
|
|
return await guild.fetchMessagesBefore(channel.id, reference.id, maxFetchElements);
|
|
|
|
}, [ guild, channel.id, maxFetchElements ]);
|
|
|
|
const fetchBelowFunc = useCallback(async (reference: Message) => {
|
|
|
|
return await guild.fetchMessagesAfter(channel.id, reference.id, maxFetchElements);
|
|
|
|
}, [ guild, channel.id, maxFetchElements ]);
|
|
|
|
return GuildSubscriptions.useMultipleGuildSubscriptionScrolling(
|
|
|
|
guild, {
|
|
|
|
newEventName: 'new-messages',
|
|
|
|
newEventArgsMap: (messages: Message[]) => messages,
|
|
|
|
updatedEventName: 'update-messages',
|
|
|
|
updatedEventArgsMap: (updatedMessages) => updatedMessages,
|
|
|
|
removedEventName: 'remove-messages',
|
|
|
|
removedEventArgsMap: (removedMessages) => removedMessages,
|
|
|
|
conflictEventName: 'conflict-messages',
|
|
|
|
conflictEventArgsMap: (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes<Message>) => changes,
|
|
|
|
sortFunc: Message.sortOrder
|
|
|
|
},
|
|
|
|
maxElements, maxFetchElements,
|
|
|
|
fetchMessagesFunc, fetchAboveFunc, fetchBelowFunc
|
|
|
|
)
|
|
|
|
}
|
2021-12-13 04:01:30 +00:00
|
|
|
}
|