working state - channel messages being prepared
This commit is contained in:
parent
601a5bd3d6
commit
df832f7294
@ -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]);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user