diff --git a/src/client/webapp/elements/require/atoms-2.ts b/src/client/webapp/elements/require/atoms-2.ts index c9b5d99..17c43cf 100644 --- a/src/client/webapp/elements/require/atoms-2.ts +++ b/src/client/webapp/elements/require/atoms-2.ts @@ -4,14 +4,15 @@ import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); import { ReactNode, useEffect } from "react"; -import { atom, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, useSetRecoilState } from "recoil"; -import { Changes, Channel, GuildMetadata, Member, Resource, ShouldNeverHappenError, Token } from "../../data-types"; +import { atom, AtomEffect, atomFamily, GetRecoilValue, Loadable, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilValueLoadable, 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 { AutoVerifierChangesType } from '../../auto-verifier'; import { Conflictable, Connectable } from '../../guild-types'; -import { IDQuery } from '../../auto-verifier-with-args'; +import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args'; import ElementsUtil from './elements-util'; +import Globals from '../../globals'; // General typescript type that infers the arguments of a function type Arguments = T extends (...args: infer A) => unknown ? A : never; @@ -113,7 +114,7 @@ interface RecoilLoadableAtomEffectParams { function createFetchValueFunc( getPromise: (recoilValue: RecoilValue) => Promise, guildId: number, - node: RecoilValue>, + node: RecoilState>, setSelf: (loadableValue: LoadableValue) => void, fetchFunc: (guild: CombinedGuild) => Promise>, ): () => Promise { @@ -185,7 +186,7 @@ function listenToSingle< let closed = false; (async () => { guild = await getPromise(guildState(guildId)); - if (guild === null) return; // TODO: This would put the atom in an infinite loading state. Look into useCallback for a potential way to prevent this... + 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 // Creates an event handler that directly applies the result of the eventArgsMap as a loadedValue into self @@ -193,16 +194,14 @@ function listenToSingle< eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, condition: (value: T) => boolean ): (Connectable & Conflictable)[XE] { - return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { - (async () => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const value = eventArgsMap(...args); - if (condition(value)) { - setSelf(createLoadedValue(value, fetchValueFunc)); - } + return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { + const selfState = await getPromise(node); + if (isLoaded(selfState)) { + const value = eventArgsMap(...args); + if (condition(value)) { + setSelf(createLoadedValue(value, fetchValueFunc)); } - })(); + } }) as (Connectable & Conflictable)[XE]; } @@ -230,12 +229,16 @@ interface MultipleEventMappingParams< > { newEventName: NE; newEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[NE]>) => Defined; + newEventCondition?: (eventArgsResult: Defined) => boolean; updatedEventName: UE; updatedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[UE]>) => Defined; + updatedEventCondition?: (eventArgsResult: Defined) => boolean; removedEventName: RE; removedEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[RE]>) => Defined; + removedEventCondition?: (eventArgsResult: Defined) => boolean; conflictEventName: CE; conflictEventArgsMap: (...args: Arguments<(Connectable & Conflictable)[CE]>) => Changes; + conflictEventCondition?: (eventArgsResult: Changes) => boolean; } function listenToMultiple< T extends { id: string }, @@ -261,49 +264,51 @@ function listenToMultiple< let closed = false; (async () => { guild = await getPromise(guildState(guildId)); - if (guild === null) return; + 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 // Useful for new-xxx, update-xxx, remove-xxx list events function createConnectableHandler( eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Defined, + condition: (eventArgsResult: Defined) => boolean, applyFunc: (value: T[], eventArgsResult: T[], sortFunc: (a: T, b: T) => number) => T[], ): (Connectable & Conflictable)[XE] { // 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 ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { - (async () => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const eventArgsResult = eventArgsMap(...args); + return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { + const selfState = await getPromise(node); + if (isLoaded(selfState)) { + const eventArgsResult = eventArgsMap(...args); + if (condition(eventArgsResult)) { const value = applyFunc(selfState.value, eventArgsResult, sortFunc); setSelf(createLoadedValue(value, fetchValueFunc)); } - })(); + } }) as (Connectable & Conflictable)[XE]; } // Useful for conflict-xxx list events function createConflictableHandler( eventArgsMap: (...args: Arguments<(Connectable & Conflictable)[XE]>) => Changes, + condition: (eventArgsResult: Changes) => boolean, applyFunc: (value: T[], eventArgsResult: Changes, sortFunc: (a: T, b: T) => number) => T[], ): (Connectable & Conflictable)[XE] { - return ((...args: Arguments<(Connectable & Conflictable)[XE]>) => { - (async () => { - const selfState = await getPromise(node); - if (isLoaded(selfState)) { - const eventArgsResult = eventArgsMap(...args); + return (async (...args: Arguments<(Connectable & Conflictable)[XE]>) => { + const selfState = await getPromise(node); + if (isLoaded(selfState)) { + const eventArgsResult = eventArgsMap(...args); + if (condition(eventArgsResult)) { const value = applyFunc(selfState.value, eventArgsResult, sortFunc); setSelf(createLoadedValue(value, fetchValueFunc)); } - })(); + } }) as (Connectable & Conflictable)[XE]; } - onNewFunc = createConnectableHandler(eventMapping.newEventArgsMap, applyNew); - onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, applyUpdated); - onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, applyRemoved); - onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, applyChanges); + onNewFunc = createConnectableHandler(eventMapping.newEventArgsMap, eventMapping.newEventCondition ?? (() => true), applyNew); + onUpdateFunc = createConnectableHandler(eventMapping.updatedEventArgsMap, eventMapping.updatedEventCondition ?? (() => true), applyUpdated); + onRemoveFunc = createConnectableHandler(eventMapping.removedEventArgsMap, eventMapping.removedEventCondition ?? (() => true), applyRemoved); + onConflictFunc = createConflictableHandler(eventMapping.conflictEventArgsMap, eventMapping.conflictEventCondition ?? (() => true), applyChanges); guild.on(eventMapping.newEventName, onNewFunc); guild.on(eventMapping.updatedEventName, onUpdateFunc); guild.on(eventMapping.removedEventName, onRemoveFunc); @@ -319,25 +324,6 @@ function listenToMultiple< return cleanup; } -// function listenToMultipleScrolling< -// 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 -// >( -// getPromise: (recoilValue: RecoilValue) => Promise, -// guildId: number, -// node: RecoilValue>, -// setSelf: (loadableValue: LoadableValue) => void, -// fetchValueFunc: () => Promise, -// fetchAboveFunc: (reference: T) => Promise, -// fetchBelowFunc: (reference: T) => Promise, -// sortFunc: (a: T, b: T) => number, -// eventMapping: MultipleEventMappingParams -// ) { -// // TODO -// } function singleGuildSubscriptionEffect< T, // e.g. GuildMetadata @@ -349,10 +335,9 @@ function singleGuildSubscriptionEffect< eventMapping: SingleEventMappingParams, skipFunc?: () => boolean ) { - return (params: RecoilLoadableAtomEffectParams) => { + const effect: AtomEffect> = ({ node, trigger, setSelf, getPromise }) => { if (skipFunc && skipFunc()) return; // Don't run if this atom should be skipped for some reason (e.g. null resourceId) - const { node, trigger, setSelf, getPromise } = params; const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); // Fetch initial value on first get @@ -367,6 +352,7 @@ function singleGuildSubscriptionEffect< cleanup(); } } + return effect; } function multipleGuildSubscriptionEffect< @@ -381,8 +367,7 @@ function multipleGuildSubscriptionEffect< sortFunc: (a: T, b: T) => number, eventMapping: MultipleEventMappingParams, ) { - return (params: RecoilLoadableAtomEffectParams) => { - const { node, trigger, setSelf, getPromise } = params; + const effect: AtomEffect> = ({ node, trigger, setSelf, getPromise }) => { const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, fetchFunc); // Fetch initial value on first get @@ -396,7 +381,40 @@ function multipleGuildSubscriptionEffect< return () => { cleanup(); } - } + }; + return effect; +} + +interface ScrollingList { + list: T[]; + hasMoreAbove: boolean; + hasMoreBelow: boolean; +} +function multipleScrollingGuildSubscriptionEffect< + T extends { id: string }, + NE extends keyof Connectable, // New Event + UE extends keyof Connectable, // Update Event + RE extends keyof Connectable, // Remove Event + CE extends keyof Conflictable // Conflict Event +>( + guildId: number, + fetchBottomFunc: (guild: CombinedGuild, count: number) => Promise, + fetchAboveFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, + fetchBelowFunc: (guild: CombinedGuild, reference: T, count: number) => Promise, + 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 +) { + const effect: AtomEffect>> = ({ node, trigger, setSelf, getPromise }) => { + const fetchValueFunc = createFetchValueFunc(getPromise, guildId, node, setSelf, async (guild: CombinedGuild) => { + const list = await fetchBottomFunc(guild, fetchCount); + return { list, hasMoreBelow: false, hasMoreAbove: list.length <= fetchCount }; + }); + + // TODO: listen for changes + }; + return effect; } // You probably want currGuildMetaState @@ -528,7 +546,7 @@ const guildChannelsState = atomFamily, number>({ }); // You probably want currGuildActiveChannel -export const guildActiveChannelIdState: (guildId: number) => RecoilState = atomFamily({ +export const guildActiveChannelIdState = atomFamily({ key: 'guildActiveChannelIdState', default: null, }); @@ -551,6 +569,40 @@ const guildActiveChannelState = selectorFamily, number>({ dangerouslyAllowMutability: true }); +export const guildChannelMessagesState = atomFamily>, { guildId: number, channelId: string }>({ + key: 'guildChannelMessagesState', + default: DEF_UNLOADED_VALUE, + effects_UNSTABLE: ({ guildId, channelId }) => [ + multipleScrollingGuildSubscriptionEffect( + guildId, + async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count), + async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesBefore(channelId, reference._order, count), + async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count), + Globals.MESSAGES_PER_REQUEST, + Globals.MAX_CURRENT_MESSAGES, + Message.sortOrder, + { + newEventName: 'new-messages', + newEventArgsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId), + newEventCondition: (messages: Message[]) => messages.length > 0, + updatedEventName: 'update-messages', + updatedEventArgsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId), + updatedEventCondition: (messages: Message[]) => messages.length > 0, + removedEventName: 'remove-messages', + removedEventArgsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId), + removedEventCondition: (messages: Message[]) => messages.length > 0, + conflictEventName: 'conflict-messages', + conflictEventArgsMap: (_query: PartialMessageListQuery, _changesType: AutoVerifierChangesType, changes: Changes) => ({ + added: changes.added.filter(message => message.channel.id === channelId), + updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId), + deleted: changes.deleted.filter(message => message.channel.id === channelId), + }), + conflictEventCondition: (changes: Changes) => changes.added.length + changes.updated.length + changes.deleted.length > 0, + } + ) + ] +}); + export const guildTokensState = atomFamily, number>({ key: 'guildTokensState', default: DEF_UNLOADED_VALUE,