working state - channel messages being prepared

This commit is contained in:
Michael Peters 2022-02-06 15:23:02 -06:00
parent 601a5bd3d6
commit df832f7294

View File

@ -5,11 +5,12 @@ const LOG = Logger.create(__filename, electronConsole);
import { ReactNode, useEffect } from "react";
import { atom, AtomEffect, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil";
import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError, Token } from "../../data-types";
import { Changes, Channel, GuildMetadata, Member, Message, Resource, ShouldNeverHappenError, Token } from "../../data-types";
import CombinedGuild from "../../guild-combined";
import GuildsManager from "../../guilds-manager";
import { Conflictable, Connectable } from '../../guild-types';
import ElementsUtil from './elements-util';
import Globals from '../../globals';
// General typescript type that infers the arguments of a function
type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
@ -22,20 +23,20 @@ export type UnloadedValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: false;
hasError: undefined;
loading: false;
};
export type LoadingValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: false;
hasError: undefined;
loading: true;
};
export type LoadedValue<T> = {
value: Defined<T>;
error: undefined;
retry: () => Promise<void>;
retry: () => Promise<void>; // Should refresh to the initial value
hasError: false;
loading: false;
};
@ -49,8 +50,8 @@ export type FailedValue = {
export type LoadableValue<T> = UnloadedValue | LoadingValue | LoadedValue<T> | FailedValue;
export type QueriedValue<T> = LoadingValue | LoadedValue<T> | FailedValue;
const DEF_UNLOADED_VALUE: UnloadedValue = { value: undefined, error: undefined, retry: undefined, hasError: false, loading: false };
const DEF_PENDED_VALUE: LoadingValue = { value: undefined, error: undefined, retry: undefined, hasError: false, loading: true };
const DEF_UNLOADED_VALUE: UnloadedValue = { value: undefined, error: undefined, retry: undefined, hasError: undefined, loading: false };
const DEF_PENDED_VALUE: LoadingValue = { value: undefined, error: undefined, retry: undefined, hasError: undefined, loading: true };
function createLoadedValue<T>(value: Defined<T>, retry: () => Promise<void>): LoadedValue<T> {
return {
value,
@ -71,7 +72,7 @@ function createFailedValue(error: unknown, retry: () => Promise<void>): FailedVa
}
export function isUnload<T>(loadableValue: LoadableValue<T>): loadableValue is UnloadedValue {
return loadableValue.value === undefined && loadableValue.hasError === false && loadableValue.loading === false;
return loadableValue.value === undefined && loadableValue.hasError === undefined && loadableValue.loading === false;
}
export function isPended<T>(loadableValue: LoadableValue<T>): loadableValue is LoadingValue {
return loadableValue.loading === true;
@ -83,6 +84,109 @@ export function isLoaded<T>(loadableValue: LoadableValue<T>): loadableValue is L
return loadableValue.value !== undefined;
}
interface UnloadedScrollingEnd {
hasMore: undefined | boolean; // Could be set to a boolean if we delete from opposite end while adding new elements
hasError: undefined;
error: undefined;
retry: undefined;
cancel: undefined;
loading: false;
}
interface LoadingScrollingEnd<T> {
hasMore: undefined | boolean;
hasError: undefined;
error: undefined;
retry: (reference: T) => Promise<void>;
cancel: () => void;
loading: true;
}
interface LoadedScrollingEnd<T> {
hasMore: boolean;
hasError: false;
error: undefined;
retry: (reference: T) => Promise<void>;
cancel: () => void;
loading: false;
}
interface FailedScrollingEnd<T> {
hasMore: undefined | boolean;
hasError: true;
error: unknown;
retry: (reference: T) => Promise<void>;
cancel: () => void;
loading: false;
}
export type LoadableScrollingEnd<T> = UnloadedScrollingEnd | LoadingScrollingEnd<T> | LoadedScrollingEnd<T> | FailedScrollingEnd<T>;
const DEF_UNLOADED_SCROLL_END: UnloadedScrollingEnd = { hasMore: undefined, hasError: undefined, error: undefined, retry: undefined, cancel: undefined, loading: false };
function createLoadingScrollingEnd<T>(retry: (reference: T) => Promise<void>, cancel: () => void): LoadingScrollingEnd<T> {
return {
hasMore: undefined,
hasError: undefined,
error: undefined,
retry,
cancel,
loading: true
};
}
function createLoadedScrollingEnd<T>(hasMore: boolean, retry: (reference: T) => Promise<void>, cancel: () => void): LoadedScrollingEnd<T> {
return {
hasMore,
hasError: false,
error: undefined,
retry,
cancel,
loading: false
};
}
function createFailedScrollingEnd<T>(error: unknown, retry: (reference: T) => Promise<void>, cancel: () => void): FailedScrollingEnd<T> {
return {
hasMore: undefined,
hasError: true,
error,
retry,
cancel,
loading: false
};
}
export function isEndUnload<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is UnloadedScrollingEnd {
return loadableScrollingEnd.hasError === undefined && loadableScrollingEnd.loading === false;
}
export function isEndPended<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is LoadingScrollingEnd<T> {
return loadableScrollingEnd.loading === true;
}
export function isEndFailed<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is FailedScrollingEnd<T> {
return loadableScrollingEnd.hasError === true;
}
export function isEndLoaded<T>(loadableScrollingEnd: LoadableScrollingEnd<T>): loadableScrollingEnd is LoadedScrollingEnd<T> {
return loadableScrollingEnd.hasError === false;
}
export type UnloadedValueScrolling = UnloadedValue & {
above: undefined;
below: undefined;
}
export type LoadingValueScrolling = LoadingValue & {
above: undefined;
below: undefined;
}
export type LoadedValueScrolling<T, E> = LoadedValue<T> & {
above: LoadableScrollingEnd<E>;
below: LoadableScrollingEnd<E>;
}
export type FailedValueScrolling = FailedValue & {
above: undefined;
below: undefined;
}
export type LoadableValueScrolling<T, E> = UnloadedValueScrolling | LoadingValueScrolling | LoadedValueScrolling<T, E> | FailedValueScrolling;
const DEF_UNLOADED_SCROLLING_VALUE: UnloadedValueScrolling = { ...DEF_UNLOADED_VALUE, above: undefined, below: undefined };
const DEF_PENDED_SCROLLING_VALUE: LoadingValueScrolling = { ...DEF_PENDED_VALUE, above: undefined, below: undefined };
function createLoadedValueScrolling<T, E>(value: Defined<T>, retry: () => Promise<void>, above: LoadableScrollingEnd<E>, below: LoadableScrollingEnd<E>): LoadedValueScrolling<T, E> {
return { ...createLoadedValue(value, retry), above, below };
}
function createFailedValueScrolling(error: unknown, retry: () => Promise<void>): FailedValueScrolling {
return { ...createFailedValue(error, retry), above: undefined, below: undefined }
}
export const overlayState = atom<ReactNode>({
key: 'overlayState',
default: null
@ -132,17 +236,101 @@ function createFetchValueFunc<T>(
return fetchValueFunc;
}
type FetchValueScrollingFunc = () => Promise<void>;
function createFetchValueScrollingFunc<T, E>(
atomEffectParam: AtomEffectParam<LoadableValueScrolling<T, E>>,
guildId: number,
count: number,
fetchFunc: (guild: CombinedGuild, count: number) => Promise<Defined<T>>,
createAboveEndFunc: (result: Defined<T>) => LoadableScrollingEnd<E>,
createBelowEndFunc: (result: Defined<T>) => LoadableScrollingEnd<E>,
): FetchValueScrollingFunc {
const { node, setSelf, getPromise } = atomEffectParam;
const fetchValueReferenceFunc = async () => {
const guild = await getPromise(guildState(guildId));
if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager
const selfState = await getPromise(node);
if (!isLoaded(selfState)) return; // Don't send a request if the base LoadableValueScrolling isn't loaded yet
setSelf(DEF_PENDED_SCROLLING_VALUE);
try {
const result = await fetchFunc(guild, count);
setSelf(createLoadedValueScrolling(
result,
fetchValueReferenceFunc,
createAboveEndFunc(result),
createBelowEndFunc(result)
));
} catch (e: unknown) {
LOG.error('unable to fetch value scrolling', e);
setSelf(createFailedValueScrolling(e, fetchValueReferenceFunc))
}
};
return fetchValueReferenceFunc;
}
type FetchValueScrollingReferenceFunc<T> = (reference: T) => Promise<void>;
function createFetchValueScrollingReferenceFunc<T>(
atomEffectParam: AtomEffectParam<LoadableValueScrolling<T[], T>>,
guildId: number,
getFunc: (selfState: LoadedValueScrolling<T[], T>) => LoadableScrollingEnd<T>,
applyEndToSelf: (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,
fetchReferenceFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>
): {
fetchValueReferenceFunc: FetchValueScrollingReferenceFunc<T>,
cancel: () => void
} {
const { node, setSelf, getPromise } = atomEffectParam;
// TODO: Improve cancellation behavior. The way it is now, we have to wait for promises to resolve before we can
// fetch below. On giga-slow internet, this may stink if you fetch messages above, cancel the messages below, and then try to scroll back down to the fetchBottomFunc
// (you'd have to wait for the bottom request to finish before it sends the next fetch)
let canceled = false;
const cancel = () => { canceled = true; };
const fetchValueReferenceFunc = async (reference: T) => {
const guild = await getPromise(guildState(guildId));
if (guild === null) return; // NOTE: This would only happen if this atom is created before its corresponding guild exists in the guildsManager
const selfState = await getPromise(node);
if (!isLoaded(selfState)) return; // Don't send a request if the base LoadableValueScrolling isn't loaded yet
const selfEnd = getFunc(selfState);
if (isEndPended(selfEnd)) return; // Don't send a request if we're already loading
canceled = false;
setSelf(applyEndToSelf(selfState, createLoadingScrollingEnd(fetchValueReferenceFunc, cancel)));
try {
const result = await fetchReferenceFunc(guild, reference, count);
if (canceled) {
setSelf(applyEndToSelf(selfState, DEF_UNLOADED_SCROLL_END));
} else {
const hasMore = result.length >= count;
setSelf(applyResultToSelf(selfState, createLoadedScrollingEnd(hasMore, fetchValueReferenceFunc, cancel), result));
}
} catch (e: unknown) {
if (canceled) {
LOG.error('unable to fetch value based on reference (but we were canceled)', e);
setSelf(applyEndToSelf(selfState, DEF_UNLOADED_SCROLL_END));
} else {
LOG.error('unable to fetch value based on reference', e);
setSelf(applyEndToSelf(selfState, createFailedScrollingEnd(e, fetchValueReferenceFunc, cancel)));
}
}
};
return { fetchValueReferenceFunc, cancel };
}
// Useful for new-xxx, update-xxx, remove-xxx, conflict-xxx list events
function createEventHandler<
V, // e.g. LoadableValue<Member[]>
ArgsMapResult, // e.g. Member[]
Addins, // e.g. FetchValueFunc to be called later to re-load 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, addins: Addins) => V,
addins: Addins,
applyFunc: (selfState: V, argsResult: ArgsMapResult) => V,
): (Connectable & Conflictable)[XE] {
const { node, setSelf, getPromise } = atomEffectParam;
// I think the typed EventEmitter class isn't ready for this level of insane type safety
@ -150,26 +338,25 @@ function createEventHandler<
return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
const selfState = await getPromise(node);
const argsResult = argsMap(...args);
setSelf(applyFunc(selfState, argsResult, addins));
setSelf(applyFunc(selfState, argsResult));
}) as (Connectable & Conflictable)[XE];
}
interface SingleEventMappingParams<
T,
V,
Addins,
UE extends keyof Connectable,
CE extends keyof Conflictable
> {
> {
updatedEvent: {
name: UE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T>;
applyFunc: (selfState: V, argsResult: Defined<T>, addins: Addins) => V;
applyFunc: (selfState: V, argsResult: Defined<T>) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
applyFunc: (selfState: V, argsResult: Defined<T>, addins: Addins) => V;
applyFunc: (selfState: V, argsResult: Defined<T>) => V;
}
}
@ -181,8 +368,7 @@ function listenToSingle<
>(
atomEffectParam: AtomEffectParam<V>,
guildId: number,
eventMapping: SingleEventMappingParams<T, V, FetchValueFunc, UE, CE>,
fetchValueFunc: FetchValueFunc,
eventMapping: SingleEventMappingParams<T, V, UE, CE>,
) {
const { getPromise } = atomEffectParam;
// Listen for updates
@ -195,8 +381,8 @@ 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 (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, fetchValueFunc);
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc, fetchValueFunc);
onUpdateFunc = createEventHandler(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.conflictEvent.name, onConflictFunc);
@ -213,31 +399,30 @@ function listenToSingle<
interface MultipleEventMappingParams<
T,
V,
Addins,
NE extends keyof Connectable,
UE extends keyof Connectable,
RE extends keyof Connectable,
CE extends keyof Conflictable
> {
> {
newEvent: {
name: NE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
updatedEvent: {
name: UE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
removedEvent: {
name: RE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>, addins: Addins) => V;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
applyFunc: (selfState: V, argsResult: Changes<T>, addins: Addins) => V;
applyFunc: (selfState: V, argsResult: Changes<T>) => V;
},
}
function listenToMultiple<
@ -246,12 +431,11 @@ function listenToMultiple<
NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update 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, FetchValueFunc, NE, UE, RE, CE>,
fetchValueFunc: FetchValueFunc,
eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>,
) {
const { getPromise } = atomEffectParam;
// Listen for updates
@ -266,10 +450,10 @@ function listenToMultiple<
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
onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc, fetchValueFunc);
onUpdateFunc = createEventHandler(atomEffectParam, eventMapping.updatedEvent.argsMap, eventMapping.updatedEvent.applyFunc, fetchValueFunc);
onRemoveFunc = createEventHandler(atomEffectParam, eventMapping.removedEvent.argsMap, eventMapping.removedEvent.applyFunc, fetchValueFunc);
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc, fetchValueFunc);
onNewFunc = createEventHandler(atomEffectParam, eventMapping.newEvent.argsMap, eventMapping.newEvent.applyFunc);
onUpdateFunc = createEventHandler(atomEffectParam, 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.updatedEvent.name, onUpdateFunc);
@ -291,12 +475,12 @@ function guildDataSubscriptionLoadableSingleEffect<
T, // e.g. GuildMetadata
UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event
>(
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
eventMapping: SingleEventMappingParams<T, LoadableValue<T>, FetchValueFunc, 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 { trigger } = atomEffectParam;
if (skipFunc && skipFunc()) return; // Don't run if this atom should be skipped for some reason (e.g. null resourceId)
@ -309,7 +493,7 @@ function guildDataSubscriptionLoadableSingleEffect<
}
// Listen to changes
const cleanup = listenToSingle(atomEffectParam, guildId, eventMapping, fetchValueFunc);
const cleanup = listenToSingle(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
@ -327,7 +511,7 @@ function guildDataSubscriptionLoadableMultipleEffect<
>(
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, FetchValueFunc, NE, UE, RE, CE>,
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, NE, UE, RE, CE>,
) {
const effect: AtomEffect<LoadableValue<T[]>> = (atomEffectParam) => {
const { trigger } = atomEffectParam;
@ -340,7 +524,7 @@ function guildDataSubscriptionLoadableMultipleEffect<
}
// Listen to changes
const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping, fetchValueFunc);
const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
@ -349,38 +533,112 @@ function guildDataSubscriptionLoadableMultipleEffect<
return effect;
}
// TODO: ScrollableLoadableValue
// interface ScrollingList<T> {
// list: T[];
// hasMoreAbove: boolean;
// hasMoreBelow: boolean;
// }
// function multipleScrollingGuildSubscriptionEffect<
// T extends { id: string },
// NE extends keyof Connectable, // New Event
// UE extends keyof Connectable, // Update Event
// RE extends keyof Connectable, // Remove Event
// CE extends keyof Conflictable // Conflict Event
// >(
// guildId: number,
// fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise<T[]>,
// fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>,
// fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[] | null>,
// fetchCount: number, // NOTE: If a fetch returns less than this number of elements, we will no longer try to get more above/below it
// maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements
// sortFunc: (a: T, b: T) => number,
// eventMapping: MultipleEventMappingParams<T, NE, UE, RE, CE>
// ) {
// const effect: AtomEffect<LoadableValue<ScrollingList<T>>> = ({ node, trigger, setSelf, getPromise }) => {
// const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, async (guild: CombinedGuild) => {
// const list = await fetchBottomFunc(guild, fetchCount);
// return { list, hasMoreBelow: false, hasMoreAbove: list.length <= fetchCount };
// });
//
// // TODO: listen for changes
// };
// return effect;
// }
interface ScrollingFetchFuncs<T> {
fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise<T[]>,
fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>,
fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise<T[]>;
}
function multipleScrollingGuildSubscriptionEffect<
T extends { id: string },
NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update Event
RE extends keyof Connectable, // Remove Event
CE extends keyof Conflictable // Conflict Event
>(
guildId: number,
fetchFuncs: ScrollingFetchFuncs<T>,
fetchCount: number, // NOTE: If a fetch returns less than this number of elements, we will no longer try to get more above/below it
maxElements: number, // The maximum number of elements in the scroller. Must be greater than maxFetchElements
sortFunc: (a: T, b: T) => number,
eventMapping: MultipleEventMappingParams<T, LoadableValueScrolling<T[], T>, NE, UE, RE, CE>
) {
const effect: AtomEffect<LoadableValueScrolling<T[], T>> = atomEffectParam => {
const { trigger } = atomEffectParam;
// Initial fetch (fetches the bottom, "most recent");
// TODO: Fetch in the middle! (this way we can keep messages in the same place when switching through channels)
const fetchValueBottomFunc = createFetchValueScrollingFunc(
atomEffectParam,
guildId,
fetchCount,
fetchFuncs.fetchBottomFunc,
(result: T[]) => createLoadedScrollingEnd(result.length >= fetchCount, fetchValueAboveReferenceFunc, cancelAbove),
(_result: T[]) => createLoadedScrollingEnd(false, fetchValueBelowReferenceFunc, cancelBelow)
);
// Fetch Above a Reference
const {
fetchValueReferenceFunc: fetchValueAboveReferenceFunc,
cancel: cancelAbove
} = createFetchValueScrollingReferenceFunc<T>(
atomEffectParam,
guildId,
(selfState: LoadedValueScrolling<T[], T>) => selfState.above,
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, above: end }), // for "pending, etc"
(selfState: LoadedValueScrolling<T[], T>, end: LoadedScrollingEnd<T>, result: T[]) => {
let nextValue = result.concat(selfState.value).sort(sortFunc);
let sliced = false;
if (nextValue.length > maxElements) {
nextValue = nextValue.slice(undefined, maxElements);
sliced = true;
}
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
const {
fetchValueReferenceFunc: fetchValueBelowReferenceFunc,
cancel: cancelBelow
} = createFetchValueScrollingReferenceFunc<T>(
atomEffectParam,
guildId,
(selfState: LoadedValueScrolling<T[], T>) => selfState.below,
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>) => ({ ...selfState, below: end }),
(selfState: LoadedValueScrolling<T[], T>, end: LoadableScrollingEnd<T>, result: T[]) => {
let nextValue = result.concat(selfState.value).sort(sortFunc);
let sliced = false;
if (nextValue.length > maxElements) {
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
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
if (trigger === 'get') {
fetchValueBottomFunc();
}
// Listen to changes
const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
}
};
return effect;
}
function applyNewElements<T>(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return list.concat(newElements).sort(sortFunc);
@ -402,22 +660,51 @@ function applyChangedElements<T extends { id: string }>(list: T[], changes: Chan
.sort(sortFunc);
}
function applyIfLoaded<T>(selfState: LoadableValue<T>, argsResult: Defined<T>, fetchValueFunc: FetchValueFunc): LoadableValue<T> {
function applyIfLoaded<T>(selfState: LoadableValue<T>, argsResult: Defined<T>): LoadableValue<T> {
if (!isLoaded(selfState)) return selfState;
return createLoadedValue(argsResult, fetchValueFunc);
return createLoadedValue(argsResult, selfState.retry);
}
function applyListFuncIfLoaded<T extends { id: string }, A>(
applyFunc: (list: T[], argsResult: A, sortFunc: (a: T, b: T) => number) => T[],
sortFunc: (a: T, b: T) => number
): (
selfState: LoadableValue<T[]>,
argsResult: A,
fetchValueFunc: FetchValueFunc
) => LoadableValue<T[]> {
return (selfState: LoadableValue<T[]>, argsResult: A, fetchValueFunc: FetchValueFunc) => {
) => LoadableValue<T[]> {
return (selfState: LoadableValue<T[]>, argsResult: A) => {
if (!isLoaded(selfState)) return selfState;
return createLoadedValue(applyFunc(selfState.value, argsResult, sortFunc), fetchValueFunc);
return createLoadedValue(applyFunc(selfState.value, argsResult, sortFunc), selfState.retry);
};
}
function applyListScrollingFuncIfLoaded<T extends { id: string }, A>(
applyFunc: (list: T[], argsResult: A, sortFunc: (a: T, b: T) => number) => T[],
sortFunc: (a: T, b: T) => number,
maxElements: number
): (
selfState: LoadableValueScrolling<T[], T>,
argsResult: A,
) => LoadableValueScrolling<T[], T> {
return (selfState: LoadableValueScrolling<T[], T>, argsResult: A) => {
if (!isLoaded(selfState)) return selfState;
let nextValue = applyFunc(selfState.value, argsResult, sortFunc);
let sliced = false;
if (nextValue.length > maxElements) {
// Slice off the top elements to make space for new elements
// TODO: in guild-subscriptions.ts, I had a way of slicing based on the scroll height.
// This would be very convenient to have. Albeit, new messages *should* be coming to the bottom anyway.
// also, deleted/updated/inserted between messages should (hopefully) not happen very much
nextValue = nextValue.slice(nextValue.length - maxElements, undefined);
sliced = true;
}
const loadedValue: LoadedValueScrolling<T[], T> = {
...selfState,
value: nextValue,
above: {
...selfState.above,
hasMore: sliced ? true : selfState.above.hasMore
} as LoadableScrollingEnd<T>,
};
return loadedValue;
};
}
@ -485,7 +772,7 @@ export const guildResourceSoftImgSrcState = selectorFamily<string, { guildId: nu
if (isFailed(resource)) return './img/error.png'; // TODO: Use BaseElements
if (!isLoaded(resource)) return './img/loading.svg'; // TODO: Use BaseElements
const imgSrc = await ElementsUtil.getImageSrcFromBufferFailSoftly(resource.value.data);
return imgSrc;
return imgSrc;
}
})
@ -605,39 +892,49 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
dangerouslyAllowMutability: true
});
// export const guildChannelMessagesState = atomFamily<LoadableValue<ScrollingList<Message>>, { guildId: number, channelId: string }>({
// key: 'guildChannelMessagesState',
// default: DEF_UNLOADED_VALUE,
// effects_UNSTABLE: ({ guildId, channelId }) => [
// multipleScrollingGuildSubscriptionEffect(
// guildId,
// async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count),
// async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesBefore(channelId, reference._order, count),
// async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count),
// Globals.MESSAGES_PER_REQUEST,
// Globals.MAX_CURRENT_MESSAGES,
// Message.sortOrder,
// {
// newEventName: 'new-messages',
// newEventArgsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId),
// newEventCondition: (messages: Message[]) => messages.length > 0,
// updatedEventName: 'update-messages',
// updatedEventArgsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId),
// updatedEventCondition: (messages: Message[]) => messages.length > 0,
// removedEventName: 'remove-messages',
// removedEventArgsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId),
// removedEventCondition: (messages: Message[]) => messages.length > 0,
// conflictEventName: 'conflict-messages',
// conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes<Message>) => ({
// added: changes.added.filter(message => message.channel.id === channelId),
// updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId),
// deleted: changes.deleted.filter(message => message.channel.id === channelId),
// }),
// conflictEventCondition: (changes: Changes<Message>) => changes.added.length + changes.updated.length + changes.deleted.length > 0,
// }
// )
// ]
// });
export const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Message[], Message>, { guildId: number, channelId: string }>({
key: 'guildChannelMessagesState',
default: DEF_UNLOADED_SCROLLING_VALUE,
effects_UNSTABLE: ({ guildId, channelId }) => [
multipleScrollingGuildSubscriptionEffect(
guildId,
{
fetchBottomFunc: async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count),
fetchAboveFunc: async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesBefore(channelId, reference._order, count),
fetchBelowFunc: async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count),
},
Globals.MESSAGES_PER_REQUEST,
Globals.MAX_CURRENT_MESSAGES,
Message.sortOrder,
{
newEvent: {
name: 'new-messages',
argsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId),
applyFunc: applyListScrollingFuncIfLoaded(applyNewElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES)
},
updatedEvent: {
name: 'update-messages',
argsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId),
applyFunc: applyListScrollingFuncIfLoaded(applyUpdatedElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES)
},
removedEvent: {
name: 'remove-messages',
argsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId),
applyFunc: applyListScrollingFuncIfLoaded(applyRemovedElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES)
},
conflictEvent: {
name: 'conflict-messages',
argsMap: (_query, _changeType, changes: Changes<Message>) => ({
added: changes.added.filter(message => message.channel.id === channelId),
updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId),
deleted: changes.deleted.filter(message => message.channel.id === channelId),
}),
applyFunc: applyListScrollingFuncIfLoaded(applyChangedElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES)
},
}
)
]
});
export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
key: 'guildTokensState',
@ -798,7 +1095,7 @@ export function initRecoil(guildsManager: GuildsManager) {
const setGuilds = useSetRecoilState(allGuildsState);
useEffect(() => {
setGuildsManager(guildsManager);
}, [ guildsManager, setGuildsManager ]);
}, [guildsManager, setGuildsManager]);
useEffect(() => {
const updateGuilds = () => { setGuilds(guildsManager.guilds.slice()); }
updateGuilds();
@ -806,6 +1103,6 @@ export function initRecoil(guildsManager: GuildsManager) {
return () => {
guildsManager.off('update-guilds', updateGuilds);
}
}, [ guildsManager ]);
}, [guildsManager]);
}