split up atoms-2 helper functions into different file

This commit is contained in:
Michael Peters 2022-02-06 17:22:49 -06:00
parent df832f7294
commit eac2c42e53
21 changed files with 807 additions and 719 deletions

View File

@ -5,8 +5,9 @@ import { Member, Token } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import Util from '../../util';
import { IAddGuildData } from '../overlays/overlay-add-guild';
import { guildMetaState, guildResourceSoftImgSrcState, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import { guildMetaState, guildResourceSoftImgSrcState, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import BaseElements from '../require/base-elements';
import { isLoaded } from '../require/loadables';
import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper';
import Button, { ButtonColorType } from './button';

View File

@ -9,7 +9,8 @@ import ChannelOverlay from '../overlays/overlay-channel';
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
import BaseElements from '../require/base-elements';
import ContextMenu from './components/context-menu';
import { currGuildSelfMemberState, currGuildState, isLoaded, overlayState } from '../require/atoms-2';
import { currGuildSelfMemberState, currGuildState, overlayState } from '../require/atoms-2';
import { isLoaded } from '../require/loadables';
export interface GuildTitleContextMenuProps {
close: () => void;

View File

@ -15,7 +15,8 @@ import Button from '../components/button';
import TokenRow from '../components/token-row';
import CombinedGuild from '../../guild-combined';
import { useRecoilValue } from 'recoil';
import { guildMetaState, guildResourceSoftImgSrcState, guildTokensState, isFailed, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import { guildMetaState, guildResourceSoftImgSrcState, guildTokensState, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import { isFailed, isLoaded } from '../require/loadables';
export interface GuildInvitesDisplayProps {
guild: CombinedGuild;

View File

@ -10,8 +10,9 @@ import Display from '../components/display';
import TextInput from '../components/input-text';
import ImageEditInput from '../components/input-image-edit';
import CombinedGuild from '../../guild-combined';
import { guildMetaState, guildResourceState, isFailed, isLoaded } from '../require/atoms-2';
import { guildMetaState, guildResourceState } from '../require/atoms-2';
import { useRecoilValue } from 'recoil';
import { isFailed, isLoaded } from '../require/loadables';
interface GuildOverviewDisplayProps {
guild: CombinedGuild;

View File

@ -1,7 +1,8 @@
import React, { FC, useMemo } from 'react'
import { useRecoilValue } from 'recoil';
import { Channel } from '../../data-types';
import { currGuildChannelsState, currGuildSelfMemberState, currGuildState, isFailed, isLoaded } from '../require/atoms-2';
import { currGuildChannelsState, currGuildSelfMemberState, currGuildState } from '../require/atoms-2';
import { isFailed, isLoaded } from '../require/loadables';
import ChannelElement from './components/channel-element';
const ChannelList: FC = () => {

View File

@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, MouseEvent, ReactNode, SetStateAction, useCallback, useRef } from 'react'
import React, { FC, MouseEvent, ReactNode, useCallback, useRef } from 'react'
import { Channel } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
import ChannelOverlay from '../../overlays/overlay-channel';
@ -11,7 +11,8 @@ import BaseElements from '../../require/base-elements';
import { useContextHover } from '../../require/react-helper';
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currGuildActiveChannelState, guildActiveChannelIdState, isLoaded, overlayState } from '../../require/atoms-2';
import { currGuildActiveChannelState, guildActiveChannelIdState, overlayState } from '../../require/atoms-2';
import { isLoaded } from '../../require/loadables';
export interface ChannelElementProps {
guild: CombinedGuild;

View File

@ -3,9 +3,10 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import CombinedGuild from '../../../guild-combined';
import ContextMenu from '../../contexts/components/context-menu';
import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic';
import { currGuildIdState, guildMetaState, guildResourceSoftImgSrcState, guildSelfMemberState, guildsManagerState, isLoaded, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
import { currGuildIdState, guildMetaState, guildResourceSoftImgSrcState, guildSelfMemberState, guildsManagerState, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
import BaseElements from '../../require/base-elements';
import { IAlignment } from '../../require/elements-util';
import { isLoaded } from '../../require/loadables';
import { useContextClickContextMenu, useContextHover } from '../../require/react-helper';
export interface GuildListElementProps {

View File

@ -5,8 +5,9 @@ import { Member, Message } from '../../../data-types';
import CombinedGuild from '../../../guild-combined';
import ImageContextMenu from '../../contexts/context-menu-image';
import ImageOverlay from '../../overlays/overlay-image';
import { guildResourceSoftImgSrcState, guildResourceState, isLoaded, overlayState, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
import { guildResourceSoftImgSrcState, guildResourceState, overlayState, useRecoilValueSoftImgSrc } from '../../require/atoms-2';
import ElementsUtil, { IAlignment } from '../../require/elements-util';
import { isLoaded } from '../../require/loadables';
import { useContextClickContextMenu, useDownloadButton, useOneTimeAsyncAction } from '../../require/react-helper';
interface ResourceElementProps {

View File

@ -7,7 +7,8 @@ import React, { FC, useMemo } from 'react';
import { Member } from '../../data-types';
import MemberElement from './components/member-element';
import { useRecoilValue } from 'recoil';
import { currGuildMembersState, currGuildState, isFailed, isPended, isUnload } from '../require/atoms-2';
import { currGuildMembersState, currGuildState } from '../require/atoms-2';
import { isFailed, isPended, isUnload } from '../require/loadables';
const MemberList: FC = () => {
const guild = useRecoilValue(currGuildState);

View File

@ -1,9 +1,17 @@
import React, { Dispatch, FC, ReactNode, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react';
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react';
import { Channel, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import MessageElement from './components/message-element';
import InfiniteScroll from '../components/infinite-scroll';
import { useMessagesScrollingSubscription } from '../require/guild-subscriptions';
import { currGuildActiveChannelMessagesState } from '../require/atoms-2';
import { useRecoilValue } from 'recoil';
import { isFailed, isLoaded, isPended, isUnload } from '../require/loadables';
interface MessageListProps {
guild: CombinedGuild;
@ -19,9 +27,23 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
const scrollToBottomFunc = useCallback(() => {
if (!infiniteScrollElementRef.current) return;
infiniteScrollElementRef.current.scrollTop = 0; // Keep in mind that this is reversed for flex-flow: column-reverse
infiniteScrollElementRef.current.scrollTop = 0; // Keep in mind that this is reversed since we are in flex-flow: column-reverse
}, []);
const recoilMessages = useRecoilValue(currGuildActiveChannelMessagesState);
useEffect(() => {
if (isUnload(recoilMessages)) {
LOG.debug('recoilMessages unloaded');
} else if (isPended(recoilMessages)) {
LOG.debug('recoilMessages pended');
} else if (isLoaded(recoilMessages)) {
LOG.debug('recoilMessages loaded. length: ' + recoilMessages.value.length);
} else if (isFailed(recoilMessages)) {
LOG.debug('recoilMessages failed');
}
// LOG.debug('recoilMessages update', { recoilMessagesLength: recoilMessages.value?.length ?? 'unloaded' });
}, [ recoilMessages ]);
// TODO: Store state higher up
const [
fetchRetryCallable,

View File

@ -4,8 +4,9 @@ import GuildInvitesDisplay from "../displays/display-guild-invites";
import GuildOverviewDisplay from "../displays/display-guild-overview";
import Overlay from '../components/overlay';
import CombinedGuild from "../../guild-combined";
import { guildMetaState, isLoaded } from "../require/atoms-2";
import { guildMetaState } from "../require/atoms-2";
import { useRecoilValue } from "recoil";
import { isLoaded } from "../require/loadables";
interface GuildSettingsOverlayProps {
guild: CombinedGuild;

View File

@ -11,7 +11,8 @@ import { useContextClickContextMenu } from '../require/react-helper';
import ImageContextMenu from '../contexts/context-menu-image';
import Overlay from '../components/overlay';
import { useRecoilValue } from 'recoil';
import { guildResourceSoftImgSrcState, guildResourceState, isFailed, isLoaded, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import { guildResourceSoftImgSrcState, guildResourceState, useRecoilValueSoftImgSrc } from '../require/atoms-2';
import { isFailed, isLoaded } from '../require/loadables';
export interface ImageOverlayProps {
guild: CombinedGuild;

View File

@ -14,7 +14,8 @@ import Overlay from '../components/overlay';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { guildResourceState, isFailed, isLoaded, overlayState } from '../require/atoms-2';
import { guildResourceState, overlayState } from '../require/atoms-2';
import { isFailed, isLoaded } from '../require/loadables';
interface PersonalizeOverlayProps {
guild: CombinedGuild;

View File

@ -4,188 +4,14 @@ import Logger from '../../../../logger/logger';
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 { atom, atomFamily, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useSetRecoilState } from "recoil";
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;
// Ensures that a type is not undefined
type Defined<T> = T extends undefined ? never : T | Awaited<T>;
type AtomEffectParam<T> = Arguments<AtomEffect<T>>[0];
export type UnloadedValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: undefined;
loading: false;
};
export type LoadingValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: undefined;
loading: true;
};
export type LoadedValue<T> = {
value: Defined<T>;
error: undefined;
retry: () => Promise<void>; // Should refresh to the initial value
hasError: false;
loading: false;
};
export type FailedValue = {
value: undefined;
error: unknown;
retry: () => Promise<void>;
hasError: true;
loading: false;
};
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: 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,
error: undefined,
retry,
hasError: false,
loading: false
};
}
function createFailedValue(error: unknown, retry: () => Promise<void>): FailedValue {
return {
value: undefined,
error,
retry,
hasError: true,
loading: false
};
}
export function isUnload<T>(loadableValue: LoadableValue<T>): loadableValue is UnloadedValue {
return loadableValue.value === undefined && loadableValue.hasError === undefined && loadableValue.loading === false;
}
export function isPended<T>(loadableValue: LoadableValue<T>): loadableValue is LoadingValue {
return loadableValue.loading === true;
}
export function isFailed<T>(loadableValue: LoadableValue<T>): loadableValue is FailedValue {
return loadableValue.hasError === true;
}
export function isLoaded<T>(loadableValue: LoadableValue<T>): loadableValue is LoadedValue<T> {
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 }
}
import { createFailedValue, createLoadedValue, DEF_PENDED_VALUE, DEF_UNLOADED_SCROLLING_VALUE, DEF_UNLOADED_VALUE, isFailed, isLoaded, isPended, isUnload, LoadableValue, LoadableValueScrolling } from './loadables';
import { applyChangedElements, applyIfLoaded, applyListFuncIfLoaded, applyListScrollingFuncIfLoaded, applyNewElements, applyRemovedElements, applyUpdatedElements, guildDataSubscriptionLoadableMultipleEffect, guildDataSubscriptionLoadableSingleEffect, multipleScrollingGuildSubscriptionEffect, useRecoilValueLoadableOrElse } from './atoms-funcs';
export const overlayState = atom<ReactNode>({
key: 'overlayState',
@ -207,507 +33,6 @@ export const allGuildsState = atom<CombinedGuild[] | null>({
dangerouslyAllowMutability: true
});
// TODO: Consider using 'getCallback' for this and having atom-backed selectors instead of using the atoms directly
// Atoms would have to be set up with destructors that unsubscribe from the guild
type FetchValueFunc = () => Promise<void>;
function createFetchValueFunc<T>(
atomEffectParam: AtomEffectParam<LoadableValue<T>>,
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): FetchValueFunc {
const { node, setSelf, getPromise } = atomEffectParam;
const fetchValueFunc = 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 (isPended(selfState)) return; // Don't send another request if we're already loading
setSelf(DEF_PENDED_VALUE);
try {
const value = await fetchFunc(guild);
setSelf(createLoadedValue(value, fetchValueFunc));
} catch (e: unknown) {
LOG.error('unable to fetch initial guild metadata', e);
setSelf(createFailedValue(e, fetchValueFunc));
}
}
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[]
XE extends keyof (Connectable | Conflictable) // e.g. new-members
>(
atomEffectParam: AtomEffectParam<V>,
argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult,
applyFunc: (selfState: V, argsResult: ArgsMapResult) => V,
): (Connectable & Conflictable)[XE] {
const { node, setSelf, getPromise } = atomEffectParam;
// 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
return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
const selfState = await getPromise(node);
const argsResult = argsMap(...args);
setSelf(applyFunc(selfState, argsResult));
}) as (Connectable & Conflictable)[XE];
}
interface SingleEventMappingParams<
T,
V,
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>) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
applyFunc: (selfState: V, argsResult: Defined<T>) => V;
}
}
function listenToSingle<
T, // e.g. GuildMetadata
V, // e.g. LoadableValue<GuildMetadata>
UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event
>(
atomEffectParam: AtomEffectParam<V>,
guildId: number,
eventMapping: SingleEventMappingParams<T, V, UE, CE>,
) {
const { getPromise } = atomEffectParam;
// Listen for updates
let guild: CombinedGuild | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null;
let onConflictFunc: (Connectable & Conflictable)[CE] | null = null;
let closed = false;
(async () => {
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
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);
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc);
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
})();
const cleanup = () => {
closed = true;
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc);
}
return cleanup;
}
interface MultipleEventMappingParams<
T,
V,
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[]>) => V;
},
updatedEvent: {
name: UE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
removedEvent: {
name: RE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
applyFunc: (selfState: V, argsResult: Changes<T>) => V;
},
}
function listenToMultiple<
T extends { id: string }, // e.g. Member
V, // e.g. LoadableValue<Member[]>
NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update Event
RE extends keyof Connectable, // Remove Event
CE extends keyof Conflictable, // Conflict Event
>(
atomEffectParam: AtomEffectParam<V>,
guildId: number,
eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>,
) {
const { getPromise } = atomEffectParam;
// Listen for updates
let guild: CombinedGuild | null = null;
let onNewFunc: (Connectable & Conflictable)[NE] | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null;
let onRemoveFunc: (Connectable & Conflictable)[RE] | null = null;
let onConflictFunc: (Connectable & Conflictable)[CE] | null = null;
let closed = false;
(async () => {
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
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);
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);
guild.on(eventMapping.removedEvent.name, onRemoveFunc);
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
})();
const cleanup = () => {
closed = true;
if (guild && onNewFunc) guild.off(eventMapping.newEvent.name, onNewFunc);
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc);
if (guild && onRemoveFunc) guild.off(eventMapping.removedEvent.name, onRemoveFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc);
}
return cleanup;
}
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>, 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)
const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc);
// Fetch initial value on first get
if (trigger === 'get') {
fetchValueFunc();
}
// Listen to changes
const cleanup = listenToSingle(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
}
}
return effect;
}
function guildDataSubscriptionLoadableMultipleEffect<
T extends { id: string },
NE extends keyof Connectable,
UE extends keyof Connectable,
RE extends keyof Connectable,
CE extends keyof Conflictable
>(
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, NE, UE, RE, CE>,
) {
const effect: AtomEffect<LoadableValue<T[]>> = (atomEffectParam) => {
const { trigger } = atomEffectParam;
const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc);
// Fetch initial value on first get
if (trigger === 'get') {
fetchValueFunc();
}
// Listen to changes
const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
}
};
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);
}
function applyUpdatedElements<T extends { id: string }>(list: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return list.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
}
function applyRemovedElements<T extends { id: string }>(list: T[], removedElements: T[]): T[] {
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id));
return list.filter(element => !removedIds.has(element.id));
}
function applyChangedElements<T extends { id: string }>(list: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id));
return list
.concat(changes.added)
.map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !removedIds.has(element.id))
.sort(sortFunc);
}
function applyIfLoaded<T>(selfState: LoadableValue<T>, argsResult: Defined<T>): LoadableValue<T> {
if (!isLoaded(selfState)) return selfState;
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,
) => LoadableValue<T[]> {
return (selfState: LoadableValue<T[]>, argsResult: A) => {
if (!isLoaded(selfState)) return selfState;
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;
};
}
// You probably want currGuildMetaState
export const guildMetaState = atomFamily<LoadableValue<GuildMetadata>, number>({
key: 'guildMetaState',
@ -892,7 +217,7 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
dangerouslyAllowMutability: true
});
export const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Message[], Message>, { guildId: number, channelId: string }>({
const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Message[], Message>, { guildId: number, channelId: string }>({
key: 'guildChannelMessagesState',
default: DEF_UNLOADED_SCROLLING_VALUE,
effects_UNSTABLE: ({ guildId, channelId }) => [
@ -933,7 +258,8 @@ export const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Messa
},
}
)
]
],
dangerouslyAllowMutability: true
});
export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
@ -969,7 +295,7 @@ export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
]
});
const guildState = selectorFamily<CombinedGuild | null, number>({
export const guildState = selectorFamily<CombinedGuild | null, number>({
key: 'guildState',
get: (guildId: number) => ({ get }) => {
const guildsManager = get(guildsManagerState);
@ -989,6 +315,7 @@ function createCurrentGuildStateGetter<T>(subSelectorFamily: (guildId: number) =
return ({ get }: { get: GetRecoilValue }) => {
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return null;
const value = get(subSelectorFamily(currGuildId));
return value;
}
@ -998,6 +325,7 @@ function createCurrentGuildHardValueWithParamStateGetter<T, P>(subSelectorFamily
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return unloadedValue;
const value = get(subSelectorFamily(guildIdToParam(currGuildId)));
if (value === null) return unloadedValue;
return value;
@ -1008,8 +336,20 @@ function createCurrentGuildLoadableStateGetter<T>(subSelectorFamily: (guildId: n
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return DEF_UNLOADED_VALUE;
const value = get(subSelectorFamily(currGuildId));
if (value === null) return DEF_UNLOADED_VALUE;
return value;
}
}
function createCurrentGuildActiveChannelLoadableScrollingStateGetter<T>(subSelectorFamily: (param: { guildId: number, channelId: string }) => RecoilState<LoadableValueScrolling<T[], T>>) {
return ({ get }: { get: GetRecoilValue }) => {
// Use the unloaded value if the current guild / current guild active channel hasn't been selected yet or doesn't exist
const guildId = get(currGuildIdState);
if (guildId === null) return DEF_UNLOADED_SCROLLING_VALUE;
const activeChannelId = get(currGuildActiveChannelIdState);
if (activeChannelId === null) return DEF_UNLOADED_SCROLLING_VALUE;
const value = get(subSelectorFamily({ guildId, channelId: activeChannelId }));
return value;
}
}
@ -1018,6 +358,7 @@ function createCurrentGuildLoadableWithParamStateGetter<T, P>(subSelectorFamily:
// Use the unloaded value if the current guild hasn't been selected yet or doesn't exist
const currGuildId = get(currGuildIdState);
if (currGuildId === null) return DEF_UNLOADED_VALUE;
const value = get(subSelectorFamily(guildIdToParam(currGuildId)));
if (value === null) return DEF_UNLOADED_VALUE;
return value;
@ -1061,30 +402,27 @@ export const currGuildChannelsState = selector<LoadableValue<Channel[]>>({
get: createCurrentGuildLoadableStateGetter(guildChannelsState),
dangerouslyAllowMutability: true
});
export const currGuildActiveChannelIdState = selector<string | null>({
key: 'currGuildActiveChannelIdState',
get: createCurrentGuildStateGetter(guildActiveChannelIdState),
dangerouslyAllowMutability: true
});
export const currGuildActiveChannelState = selector<LoadableValue<Channel>>({
key: 'currGuildActiveChannelState',
get: createCurrentGuildLoadableStateGetter(guildActiveChannelState),
dangerouslyAllowMutability: true
});
export const currGuildActiveChannelMessagesState = selector<LoadableValueScrolling<Message[], Message>>({
key: 'currGuildActiveChannelMessagesState',
get: createCurrentGuildActiveChannelLoadableScrollingStateGetter(guildChannelMessagesState),
dangerouslyAllowMutability: true
});
export const currGuildTokensState = selector<LoadableValue<Token[]>>({
key: 'currGuildTokensState',
get: createCurrentGuildLoadableStateGetter(guildTokensState)
get: createCurrentGuildLoadableStateGetter(guildTokensState),
dangerouslyAllowMutability: true
});
// Helper functions for using softImgSrc states
function useLoadedOrElse<T>(loadable: Loadable<T>, ifLoading: T, ifError: T) {
if (loadable.state === 'hasValue') {
return loadable.contents;
} else if (loadable.state === 'loading') {
return ifLoading;
} else {
return ifError;
}
}
function useRecoilValueLoadableOrElse<T>(recoilValue: RecoilValue<T>, ifLoading: T, ifError: T): T {
const loadableValue = useRecoilValueLoadable(recoilValue);
return useLoadedOrElse(loadableValue, ifLoading, ifError);
}
export function useRecoilValueSoftImgSrc(recoilValue: RecoilValue<string>): string {
return useRecoilValueLoadableOrElse(recoilValue, './img/loading.svg', './img/error.png');
}
@ -1095,7 +433,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();
@ -1103,6 +441,6 @@ export function initRecoil(guildsManager: GuildsManager) {
return () => {
guildsManager.off('update-guilds', updateGuilds);
}
}, [guildsManager]);
}, [ guildsManager ]);
}

View File

@ -0,0 +1,536 @@
import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { AtomEffect, Loadable, RecoilValue, useRecoilValueLoadable } from "recoil";
import CombinedGuild from "../../guild-combined";
import { Conflictable, Connectable } from "../../guild-types";
import { createFailedScrollingEnd, createFailedValue, createFailedValueScrolling, createLoadedScrollingEnd, createLoadedValue, createLoadedValueScrolling, createLoadingScrollingEnd, Defined, DEF_PENDED_SCROLLING_VALUE, DEF_PENDED_VALUE, DEF_UNLOADED_SCROLL_END, isEndPended, isLoaded, isPended, LoadableScrollingEnd, LoadableValue, LoadableValueScrolling, LoadedScrollingEnd, LoadedValueScrolling } from "./loadables";
import { guildState } from './atoms-2';
import { Changes } from '../../data-types';
// General typescript type that infers the arguments of a function
export type Arguments<T> = T extends (...args: infer A) => unknown ? A : never;
export type AtomEffectParam<T> = Arguments<AtomEffect<T>>[0];
// TODO: Consider using 'getCallback' for this and having atom-backed selectors instead of using the atoms directly
// Atoms would have to be set up with destructors that unsubscribe from the guild
// "initial" value loaders
export type FetchValueFunc = () => Promise<void>;
export function createFetchValueFunc<T>(
atomEffectParam: AtomEffectParam<LoadableValue<T>>,
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T>>,
): FetchValueFunc {
const { node, setSelf, getPromise } = atomEffectParam;
const fetchValueFunc = 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 (isPended(selfState)) return; // Don't send another request if we're already loading
setSelf(DEF_PENDED_VALUE);
try {
const value = await fetchFunc(guild);
setSelf(createLoadedValue(value, fetchValueFunc));
} catch (e: unknown) {
LOG.error('unable to fetch initial guild metadata', e);
setSelf(createFailedValue(e, fetchValueFunc));
}
}
return fetchValueFunc;
}
export type FetchValueScrollingFunc = () => Promise<void>;
export 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 (isPended(selfState)) return; // Don't send another request if we're already loading
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;
}
export type FetchValueScrollingReferenceFunc<T> = (reference: T) => Promise<void>;
export 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 };
}
// Generalized Event Handler
// Useful for new-xxx, update-xxx, remove-xxx, conflict-xxx list events
function createEventHandler<
V, // e.g. LoadableValue<Member[]>
ArgsMapResult, // e.g. Member[]
XE extends keyof (Connectable | Conflictable) // e.g. new-members
>(
atomEffectParam: AtomEffectParam<V>,
argsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => ArgsMapResult,
applyFunc: (selfState: V, argsResult: ArgsMapResult) => V,
): (Connectable & Conflictable)[XE] {
const { node, setSelf, getPromise } = atomEffectParam;
// 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
return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => {
const selfState = await getPromise(node);
const argsResult = argsMap(...args);
setSelf(applyFunc(selfState, argsResult));
}) as (Connectable & Conflictable)[XE];
}
// Subscription Effects
export interface SingleEventMappingParams<
T,
V,
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>) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Defined<T>;
applyFunc: (selfState: V, argsResult: Defined<T>) => V;
}
}
export function listenToSingle<
T, // e.g. GuildMetadata
V, // e.g. LoadableValue<GuildMetadata>
UE extends keyof Connectable, // Update Event
CE extends keyof Conflictable, // Conflict Event
>(
atomEffectParam: AtomEffectParam<V>,
guildId: number,
eventMapping: SingleEventMappingParams<T, V, UE, CE>,
) {
const { getPromise } = atomEffectParam;
// Listen for updates
let guild: CombinedGuild | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null;
let onConflictFunc: (Connectable & Conflictable)[CE] | null = null;
let closed = false;
(async () => {
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
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);
onConflictFunc = createEventHandler(atomEffectParam, eventMapping.conflictEvent.argsMap, eventMapping.conflictEvent.applyFunc);
guild.on(eventMapping.updatedEvent.name, onUpdateFunc);
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
})();
const cleanup = () => {
closed = true;
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc);
}
return cleanup;
}
export interface MultipleEventMappingParams<
T,
V,
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[]>) => V;
},
updatedEvent: {
name: UE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
removedEvent: {
name: RE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined<T[]>;
applyFunc: (selfState: V, argsResult: Defined<T[]>) => V;
},
conflictEvent: {
name: CE;
argsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes<T>;
applyFunc: (selfState: V, argsResult: Changes<T>) => V;
},
}
export function listenToMultiple<
T extends { id: string }, // e.g. Member
V, // e.g. LoadableValue<Member[]>
NE extends keyof Connectable, // New Event
UE extends keyof Connectable, // Update Event
RE extends keyof Connectable, // Remove Event
CE extends keyof Conflictable, // Conflict Event
>(
atomEffectParam: AtomEffectParam<V>,
guildId: number,
eventMapping: MultipleEventMappingParams<T, V, NE, UE, RE, CE>,
) {
const { getPromise } = atomEffectParam;
// Listen for updates
let guild: CombinedGuild | null = null;
let onNewFunc: (Connectable & Conflictable)[NE] | null = null;
let onUpdateFunc: (Connectable & Conflictable)[UE] | null = null;
let onRemoveFunc: (Connectable & Conflictable)[RE] | null = null;
let onConflictFunc: (Connectable & Conflictable)[CE] | null = null;
let closed = false;
(async () => {
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
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);
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);
guild.on(eventMapping.removedEvent.name, onRemoveFunc);
guild.on(eventMapping.conflictEvent.name, onConflictFunc);
})();
const cleanup = () => {
closed = true;
if (guild && onNewFunc) guild.off(eventMapping.newEvent.name, onNewFunc);
if (guild && onUpdateFunc) guild.off(eventMapping.updatedEvent.name, onUpdateFunc);
if (guild && onRemoveFunc) guild.off(eventMapping.removedEvent.name, onRemoveFunc);
if (guild && onConflictFunc) guild.off(eventMapping.conflictEvent.name, onConflictFunc);
}
return cleanup;
}
export 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>, 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)
const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc);
// Fetch initial value on first get
if (trigger === 'get') {
fetchValueFunc();
}
// Listen to changes
const cleanup = listenToSingle(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
}
}
return effect;
}
export function guildDataSubscriptionLoadableMultipleEffect<
T extends { id: string },
NE extends keyof Connectable,
UE extends keyof Connectable,
RE extends keyof Connectable,
CE extends keyof Conflictable
>(
guildId: number,
fetchFunc: (guild: CombinedGuild) => Promise<Defined<T[]>>,
eventMapping: MultipleEventMappingParams<T, LoadableValue<T[]>, NE, UE, RE, CE>,
) {
const effect: AtomEffect<LoadableValue<T[]>> = (atomEffectParam) => {
const { trigger } = atomEffectParam;
const fetchValueFunc = createFetchValueFunc(atomEffectParam, guildId, fetchFunc);
// Fetch initial value on first get
if (trigger === 'get') {
fetchValueFunc();
}
// Listen to changes
const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
}
};
return effect;
}
export 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[]>;
}
export 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: LoadedScrollingEnd<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') {
LOG.debug('fetching scrolling bottom...');
fetchValueBottomFunc();
}
// Listen to changes
const cleanup = listenToMultiple(atomEffectParam, guildId, eventMapping);
return () => {
cleanup();
}
};
return effect;
}
// List management helper functions
export function applyNewElements<T>(list: T[], newElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return list.concat(newElements).sort(sortFunc);
}
export function applyUpdatedElements<T extends { id: string }>(list: T[], updatedElements: T[], sortFunc: (a: T, b: T) => number): T[] {
return list.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc);
}
export function applyRemovedElements<T extends { id: string }>(list: T[], removedElements: T[]): T[] {
const removedIds = new Set<string>(removedElements.map(removedElement => removedElement.id));
return list.filter(element => !removedIds.has(element.id));
}
export function applyChangedElements<T extends { id: string }>(list: T[], changes: Changes<T>, sortFunc: (a: T, b: T) => number): T[] {
const removedIds = new Set<string>(changes.deleted.map(deletedElement => deletedElement.id));
return list
.concat(changes.added)
.map(element => changes.updated.find(updatedPair => updatedPair.newDataPoint.id === element.id)?.newDataPoint ?? element)
.filter(element => !removedIds.has(element.id))
.sort(sortFunc);
}
export function applyIfLoaded<T>(selfState: LoadableValue<T>, argsResult: Defined<T>): LoadableValue<T> {
if (!isLoaded(selfState)) return selfState;
return createLoadedValue(argsResult, selfState.retry);
}
export 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,
) => LoadableValue<T[]> {
return (selfState: LoadableValue<T[]>, argsResult: A) => {
if (!isLoaded(selfState)) return selfState;
return createLoadedValue(applyFunc(selfState.value, argsResult, sortFunc), selfState.retry);
};
}
export 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;
};
}
// Helper functions for using softImgSrc states
export function useLoadedOrElse<T>(loadable: Loadable<T>, ifLoading: T, ifError: T) {
if (loadable.state === 'hasValue') {
return loadable.contents;
} else if (loadable.state === 'loading') {
return ifLoading;
} else {
return ifError;
}
}
export function useRecoilValueLoadableOrElse<T>(recoilValue: RecoilValue<T>, ifLoading: T, ifError: T): T {
const loadableValue = useRecoilValueLoadable(recoilValue);
return useLoadedOrElse(loadableValue, ifLoading, ifError);
}

View File

@ -1,6 +1,6 @@
import React from 'react';
// This file contains JSX elements that can be used in place directly of storing the elements on the disk.
// This file contains JSX elements that can be used in place of storing the elements on the disk.
export default class BaseElements {
// Scraped directly from discord (#)

View File

@ -703,7 +703,6 @@ function useMultipleGuildSubscriptionScrolling<
}
/**
* @deprecated
* @param guild The guild to load from
* @returns [
* guildMetaResult: The guild's metadata

View File

@ -0,0 +1,170 @@
export type Defined<T> = T extends undefined ? never : T | Awaited<T>;
export type UnloadedValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: undefined;
loading: false;
};
export type LoadingValue = {
value: undefined;
error: undefined;
retry: undefined;
hasError: undefined;
loading: true;
};
export type LoadedValue<T> = {
value: Defined<T>;
error: undefined;
retry: () => Promise<void>; // Should refresh to the initial value
hasError: false;
loading: false;
};
export type FailedValue = {
value: undefined;
error: unknown;
retry: () => Promise<void>;
hasError: true;
loading: false;
};
export type LoadableValue<T> = UnloadedValue | LoadingValue | LoadedValue<T> | FailedValue;
export type QueriedValue<T> = LoadingValue | LoadedValue<T> | FailedValue;
export const DEF_UNLOADED_VALUE: UnloadedValue = { value: undefined, error: undefined, retry: undefined, hasError: undefined, loading: false };
export const DEF_PENDED_VALUE: LoadingValue = { value: undefined, error: undefined, retry: undefined, hasError: undefined, loading: true };
export function createLoadedValue<T>(value: Defined<T>, retry: () => Promise<void>): LoadedValue<T> {
return {
value,
error: undefined,
retry,
hasError: false,
loading: false
};
}
export function createFailedValue(error: unknown, retry: () => Promise<void>): FailedValue {
return {
value: undefined,
error,
retry,
hasError: true,
loading: false
};
}
export function isUnload<T>(loadableValue: LoadableValue<T>): loadableValue is UnloadedValue {
return loadableValue.value === undefined && loadableValue.hasError === undefined && loadableValue.loading === false;
}
export function isPended<T>(loadableValue: LoadableValue<T>): loadableValue is LoadingValue {
return loadableValue.loading === true;
}
export function isFailed<T>(loadableValue: LoadableValue<T>): loadableValue is FailedValue {
return loadableValue.hasError === true;
}
export function isLoaded<T>(loadableValue: LoadableValue<T>): loadableValue is LoadedValue<T> {
return loadableValue.value !== undefined;
}
export 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;
}
export interface LoadingScrollingEnd<T> {
hasMore: undefined | boolean;
hasError: undefined;
error: undefined;
retry: (reference: T) => Promise<void>;
cancel: () => void;
loading: true;
}
export interface LoadedScrollingEnd<T> {
hasMore: boolean;
hasError: false;
error: undefined;
retry: (reference: T) => Promise<void>;
cancel: () => void;
loading: false;
}
export 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>;
export const DEF_UNLOADED_SCROLL_END: UnloadedScrollingEnd = { hasMore: undefined, hasError: undefined, error: undefined, retry: undefined, cancel: undefined, loading: false };
export function createLoadingScrollingEnd<T>(retry: (reference: T) => Promise<void>, cancel: () => void): LoadingScrollingEnd<T> {
return {
hasMore: undefined,
hasError: undefined,
error: undefined,
retry,
cancel,
loading: true
};
}
export function createLoadedScrollingEnd<T>(hasMore: boolean, retry: (reference: T) => Promise<void>, cancel: () => void): LoadedScrollingEnd<T> {
return {
hasMore,
hasError: false,
error: undefined,
retry,
cancel,
loading: false
};
}
export 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;
export const DEF_UNLOADED_SCROLLING_VALUE: UnloadedValueScrolling = { ...DEF_UNLOADED_VALUE, above: undefined, below: undefined };
export const DEF_PENDED_SCROLLING_VALUE: LoadingValueScrolling = { ...DEF_PENDED_VALUE, above: undefined, below: undefined };
export 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 };
}
export function createFailedValueScrolling(error: unknown, retry: () => Promise<void>): FailedValueScrolling {
return { ...createFailedValue(error, retry), above: undefined, below: undefined }
}

View File

@ -4,7 +4,8 @@ import MemberElement, { DummyMember } from '../lists/components/member-element';
import ConnectionInfoContextMenu from '../contexts/context-menu-connection-info';
import { useContextMenu } from '../require/react-helper';
import { useRecoilValue } from 'recoil';
import { currGuildSelfMemberState, currGuildState, isFailed, isLoaded, isPended, isUnload } from '../require/atoms-2';
import { currGuildSelfMemberState, currGuildState } from '../require/atoms-2';
import { isFailed, isLoaded, isPended, isUnload } from '../require/loadables';
const ConnectionInfo: FC = () => {
const rootRef = useRef<HTMLDivElement>(null);

View File

@ -1,7 +1,8 @@
import React, { FC, useMemo, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import GuildTitleContextMenu from '../contexts/context-menu-guild-title';
import { currGuildMetaState, currGuildSelfMemberState, isLoaded } from '../require/atoms-2';
import { currGuildMetaState, currGuildSelfMemberState } from '../require/atoms-2';
import { isLoaded } from '../require/loadables';
import { useContextMenu } from '../require/react-helper';
const GuildTitle: FC = () => {

View File

@ -13,7 +13,8 @@ import ConnectionInfo from './connection-info';
import GuildTitle from './guild-title';
import SendMessage from './send-message';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currGuildActiveChannelState, currGuildChannelsState, currGuildSelfMemberState, currGuildState, guildActiveChannelIdState, isLoaded, isUnload } from '../require/atoms-2';
import { currGuildActiveChannelState, currGuildChannelsState, currGuildSelfMemberState, currGuildState, guildActiveChannelIdState } from '../require/atoms-2';
import { isLoaded, isUnload } from '../require/loadables';
const GuildElement: FC = () => {
// TODO: Handle fetch errors by allowing for retry
@ -26,6 +27,14 @@ const GuildElement: FC = () => {
const activeChannel = useRecoilValue(currGuildActiveChannelState);
const setActiveChannelId = useSetRecoilState(guildActiveChannelIdState(guild?.id ?? -1));
// useEffect(() => {
// LOG.debug('guild changed', { guildId: guild?.id ?? '<null>' });
// }, [ guild ]);
// useEffect(() => {
// LOG.debug('active channel changed', { activeChannel });
// }, [ activeChannel ])
const [ fetchMessagesRetryCallable, setFetchMessagesRetryCallable ] = useState<(() => Promise<void>) | null>(null);
// If the active channel isn't set yet, set it to the first of the channels