split up atoms-2 helper functions into different file
This commit is contained in:
parent
df832f7294
commit
eac2c42e53
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 = () => {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 ]);
|
||||
}
|
||||
|
||||
|
536
src/client/webapp/elements/require/atoms-funcs.ts
Normal file
536
src/client/webapp/elements/require/atoms-funcs.ts
Normal 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);
|
||||
}
|
||||
|
@ -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 (#)
|
||||
|
@ -703,7 +703,6 @@ function useMultipleGuildSubscriptionScrolling<
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param guild The guild to load from
|
||||
* @returns [
|
||||
* guildMetaResult: The guild's metadata
|
||||
|
170
src/client/webapp/elements/require/loadables.ts
Normal file
170
src/client/webapp/elements/require/loadables.ts
Normal 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 }
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user