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 { Changes, GuildMetadata, Member, Message, Resource } from "../../data-types"; import CombinedGuild from "../../guild-combined"; import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AutoVerifierChangesType } from "../../auto-verifier"; import { Conflictable, Connectable } from "../../guild-types"; import { EventEmitter } from 'tsee'; import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args'; import { Token, Channel } from '../../data-types'; import { useIsMountedRef, useOneTimeAsyncAction } from './react-helper'; import Globals from '../../globals'; import ElementsUtil from './elements-util'; export type SingleSubscriptionEvents = { 'fetch': () => void; 'updated': () => void; 'conflict': () => void; 'fetch-error': () => void; } export type MultipleSubscriptionEvents = { 'fetch': () => void; 'fetch-error': () => void; 'new': (newValues: T[]) => void; 'updated': (updatedValues: T[]) => void; 'removed': (removedValues: T[]) => void; 'conflict': (changes: Changes) => void; } interface EffectParams { guild: CombinedGuild; onFetch: (value: T | null, valueGuild: CombinedGuild) => void; onFetchError: (e: unknown) => void; bindEventsFunc: () => void; unbindEventsFunc: () => void; } type Arguments = T extends (...args: infer A) => unknown ? A : never; interface SingleEventMappingParams { updatedEventName: UE; updatedEventArgsMap: (...args: Arguments) => T; conflictEventName: CE; conflictEventArgsMap: (...args: Arguments) => T; } interface MultipleEventMappingParams< T, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable > { newEventName: NE; newEventArgsMap: (...args: Arguments) => T[]; // list of new elements updatedEventName: UE; updatedEventArgsMap: (...args: Arguments) => T[]; // list of updated elements removedEventName: RE; removedEventArgsMap: (...args: Arguments) => T[]; // list of removed elements conflictEventName: CE; conflictEventArgsMap: (...args: Arguments) => Changes; sortFunc: (a: T, b: T) => number; // Friendly reminder that v8 uses timsort so this is O(n) for pre-sorted stuff } function useGuildSubscriptionEffect( subscriptionParams: EffectParams, fetchFunc: (() => Promise) | (() => Promise) ): [ fetchRetryCallable: () => Promise ] { const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams; const isMounted = useIsMountedRef(); const guildRef = useRef(guild); guildRef.current = guild; const fetchManagerFunc = useCallback(async () => { if (!isMounted.current) return; if (guildRef.current !== guild) return; try { const value = await fetchFunc(); if (!isMounted.current) return; if (guildRef.current !== guild) return; // Don't call onFetch if we changed guilds. TODO: Test this onFetch(value, guild); } catch (e: unknown) { LOG.error('error fetching for subscription', e); if (!isMounted.current) return; if (guildRef.current !== guild) return; onFetchError(e); } }, [ fetchFunc ]); useEffect(() => { // Bind guild events to make sure we have the most up to date information guild.on('connect', fetchManagerFunc); guild.on('disconnect', fetchManagerFunc); bindEventsFunc(); // Fetch the data once fetchManagerFunc(); return () => { // Unbind the events so that we don't have any memory leaks guild.off('connect', fetchManagerFunc); guild.off('disconnect', fetchManagerFunc); unbindEventsFunc(); } }, [ fetchManagerFunc ]); return [ fetchManagerFunc ]; } function useSingleGuildSubscription( guild: CombinedGuild, eventMappingParams: SingleEventMappingParams, fetchFunc: (() => Promise) | (() => Promise) ): [value: T | null, valueGuild: CombinedGuild | null, fetchError: unknown | null, events: EventEmitter] { const { updatedEventName, updatedEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams; const isMounted = useIsMountedRef(); const guildRef = useRef(guild); guildRef.current = guild; const [ fetchError, setFetchError ] = useState(null); const [ value, setValue ] = useState(null); const [ valueGuild, setValueGuild ] = useState(null); const events = useMemo(() => new EventEmitter(), []); const onFetch = useCallback((fetchValue: T | null, fetchValueGuild: CombinedGuild) => { setValue(fetchValue); setValueGuild(fetchValueGuild); setFetchError(null); events.emit('fetch'); }, []); const onFetchError = useCallback((e: unknown) => { setFetchError(e); setValue(null); setValueGuild(null); events.emit('fetch-error'); }, []); const onUpdated = useCallback((updateValue: T, updateValueGuild: CombinedGuild) => { setValue(updateValue); if (updateValueGuild !== guildRef.current) { LOG.warn(`update guild (${updateValueGuild.id}) != current guild (${guildRef.current})`); } events.emit('updated'); }, []); const onConflict = useCallback((conflictValue: T, conflictValueGuild: CombinedGuild) => { setValue(conflictValue); if (conflictValueGuild !== guildRef.current) { LOG.warn(`conflict guild (${conflictValueGuild.id}) != current guild (${guildRef.current})`); } events.emit('conflict'); }, []); // 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 const boundUpdateFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; const value = updatedEventArgsMap(...args); onUpdated(value, guild); }, [ guild ]) as (Connectable & Conflictable)[UE]; const boundConflictFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; const value = conflictEventArgsMap(...args); onConflict(value, guild); }, [ guild ]) as (Connectable & Conflictable)[CE]; const bindEventsFunc = useCallback(() => { guild.on(updatedEventName, boundUpdateFunc); guild.on(conflictEventName, boundConflictFunc); }, [ boundUpdateFunc, boundConflictFunc ]); const unbindEventsFunc = useCallback(() => { guild.off(updatedEventName, boundUpdateFunc); guild.off(conflictEventName, boundConflictFunc); }, [ boundUpdateFunc, boundConflictFunc ]); useGuildSubscriptionEffect({ guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc }, fetchFunc); return [ value, valueGuild, fetchError, events ]; } function useMultipleGuildSubscription< T extends { id: string }, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable >( guild: CombinedGuild, eventMappingParams: MultipleEventMappingParams, fetchFunc: (() => Promise) | (() => Promise) ): [ fetchRetryCallable: () => Promise, value: T[] | null, valueGuild: CombinedGuild | null, fetchError: unknown | null, events: EventEmitter> ] { const { newEventName, newEventArgsMap, updatedEventName, updatedEventArgsMap, removedEventName, removedEventArgsMap, conflictEventName, conflictEventArgsMap, sortFunc } = eventMappingParams; const isMounted = useIsMountedRef(); const guildRef = useRef(guild); guildRef.current = guild; const [ fetchError, setFetchError ] = useState(null); const [ value, setValue ] = useState(null); const [ valueGuild, setValueGuild ] = useState(null); const events = useMemo(() => new EventEmitter>(), []); const onFetch = useCallback((fetchValue: T[] | null, fetchValueGuild: CombinedGuild) => { if (fetchValue) fetchValue.sort(sortFunc); setValue(fetchValue); setValueGuild(fetchValueGuild); setFetchError(null); events.emit('fetch'); }, [ sortFunc ]); const onFetchError = useCallback((e: unknown) => { setFetchError(e); setValue(null); events.emit('fetch-error'); }, []); const onNew = useCallback((newElements: T[], newElementsGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return Array.from(newElements).sort(sortFunc); return currentValue.concat(newElements).sort(sortFunc); }); if (newElementsGuild !== guildRef.current) { LOG.warn(`new elements guild (${newElementsGuild.id}) != current guild (${guildRef.current})`); } events.emit('new', newElements); }, [ sortFunc ]); const onUpdated = useCallback((updatedElements: T[], updatedElementsGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc); }); if (updatedElementsGuild !== guildRef.current) { LOG.warn(`updated elements guild (${updatedElementsGuild.id}) != current guild (${guildRef.current})`); } events.emit('updated', updatedElements); }, [ sortFunc ]); const onRemoved = useCallback((removedElements: T[], removedElementsGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id)); return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc); }); if (removedElementsGuild !== guildRef.current) { LOG.warn(`removed elements guild (${removedElementsGuild.id}) != current guild (${guildRef.current})`); } events.emit('removed', removedElements); }, [ sortFunc ]); const onConflict = useCallback((changes: Changes, changesGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)); return currentValue .concat(changes.added) .map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element) .filter(element => !deletedIds.has(element.id)) .sort(sortFunc); }); if (changesGuild !== guildRef.current) { LOG.warn(`conflict changes guild (${changesGuild.id}) != current guild (${guildRef.current})`); } setValueGuild(changesGuild); events.emit('conflict', changes); }, [ sortFunc ]); // 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 const boundNewFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; // prevent changes from a different guild onNew(newEventArgsMap(...args), guild); }, [ guild, onNew, newEventArgsMap ]) as (Connectable & Conflictable)[NE]; const boundUpdateFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; onUpdated(updatedEventArgsMap(...args), guild); }, [ guild, onUpdated, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE]; const boundRemovedFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; onRemoved(removedEventArgsMap(...args), guild); }, [ guild, onRemoved, removedEventArgsMap ]) as (Connectable & Conflictable)[RE]; const boundConflictFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; onConflict(conflictEventArgsMap(...args), guild); }, [ guild, onConflict, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE]; const bindEventsFunc = useCallback(() => { guild.on(newEventName, boundNewFunc); guild.on(updatedEventName, boundUpdateFunc); guild.on(removedEventName, boundRemovedFunc); guild.on(conflictEventName, boundConflictFunc); }, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]); const unbindEventsFunc = useCallback(() => { guild.off(newEventName, boundNewFunc); guild.off(updatedEventName, boundUpdateFunc); guild.off(removedEventName, boundRemovedFunc); guild.off(conflictEventName, boundConflictFunc); }, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]); const [ fetchRetryCallable ] = useGuildSubscriptionEffect({ guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc }, fetchFunc); return [ fetchRetryCallable, value, valueGuild, fetchError, events ]; } function useMultipleGuildSubscriptionScrolling< T extends { id: string }, NE extends keyof Connectable, UE extends keyof Connectable, RE extends keyof Connectable, CE extends keyof Conflictable >( guild: CombinedGuild, eventMappingParams: MultipleEventMappingParams, maxElements: number, maxFetchElements: number, fetchFunc: (() => Promise) | (() => Promise), fetchAboveFunc: ((reference: T) => Promise), fetchBelowFunc: ((reference: T) => Promise), ): [ fetchRetryCallable: () => Promise, fetchAboveCallable: () => Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }>, fetchBelowCallable: () => Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }>, setScrollRatio: Dispatch>, fetchResult: { hasMoreAbove: boolean, hasMoreBelow: boolean } | null, value: T[] | null, valueGuild: CombinedGuild | null, fetchError: unknown | null, fetchAboveError: unknown | null, fetchBelowError: unknown | null, events: EventEmitter> ] { const { newEventName, newEventArgsMap, updatedEventName, updatedEventArgsMap, removedEventName, removedEventArgsMap, conflictEventName, conflictEventArgsMap, sortFunc } = eventMappingParams; const isMounted = useIsMountedRef(); const guildRef = useRef(guild); guildRef.current = guild; const [ value, setValue ] = useState(null); const [ valueGuild, setValueGuild ] = useState(null); const [ fetchError, setFetchError ] = useState(null); const [ fetchAboveError, setFetchAboveError ] = useState(null); const [ fetchBelowError, setFetchBelowError ] = useState(null); const [ fetchResult, setFetchResult ] = useState<{ hasMoreAbove: boolean, hasMoreBelow: boolean } | null>(null); // Percentage of scroll from top. i.e. 300px from top of 1000px scroll = 0.3 scroll ratio const [ scrollRatio, setScrollRatio ] = useState(0.5); // Gets the number of elements to remove from the top or bottom. Tries to optimise so that if 10 elements // are removed and we're 30% scrolled down, 3 get removed from the top and 7 are removed from the bottom. // TBH, this function is pretty overkill but that's important const getRemoveCounts = useCallback((toRemove: number): { fromTop: number, fromBottom: number } => { // Make sure we round toward the side of the scrollbar that we are not closest to // this is important for the most common case of 1 removed element. const topRoundFunc = scrollRatio > 0.5 ? Math.ceil : Math.floor; const bottomRoundFunc = scrollRatio > 0.5 ? Math.floor : Math.ceil; return { fromTop: topRoundFunc(toRemove * scrollRatio), fromBottom: bottomRoundFunc(toRemove * scrollRatio) } }, [ scrollRatio ]); function removeByCounts(elements: T[], counts: { fromTop: number, fromBottom: number }): T[] { const { fromTop, fromBottom } = counts; return elements.slice(fromTop, elements.length - fromBottom); } const events = useMemo(() => new EventEmitter>(), []); const fetchAboveCallable = useCallback(async (): Promise<{ hasMoreAbove: boolean, removedFromBottom: boolean }> => { if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false }; if (guildRef.current !== guild) return { hasMoreAbove: false, removedFromBottom: false }; if (!value || value.length === 0) return { hasMoreAbove: false, removedFromBottom: false }; try { const reference = value[0] as T; const aboveElements = await fetchAboveFunc(reference); if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false }; if (guildRef.current !== guild) return { hasMoreAbove: false, removedFromBottom: false }; setFetchAboveError(null); if (aboveElements) { const hasMoreAbove = aboveElements.length >= maxFetchElements; let removedFromBottom = false; setValue(currentValue => { let newValue = aboveElements.concat(currentValue ?? []).sort(sortFunc); if (newValue.length > maxElements) { newValue = newValue.slice(0, maxElements); removedFromBottom = true; } return newValue; }); return { hasMoreAbove, removedFromBottom }; } else { return { hasMoreAbove: false, removedFromBottom: false }; } } catch (e: unknown) { LOG.error('error fetching above for subscription', e); if (!isMounted.current) return { hasMoreAbove: false, removedFromBottom: false }; if (guildRef.current !== guild) return { hasMoreAbove: false, removedFromBottom: false }; setFetchAboveError(e); return { hasMoreAbove: true, removedFromBottom: false }; } }, [ guild, value, fetchAboveFunc, maxFetchElements ]); const fetchBelowCallable = useCallback(async (): Promise<{ hasMoreBelow: boolean, removedFromTop: boolean }> => { if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false }; if (guildRef.current !== guild) return { hasMoreBelow: false, removedFromTop: false }; if (!value || value.length === 0) return { hasMoreBelow: false, removedFromTop: false }; try { const reference = value[value.length - 1] as T; const belowElements = await fetchBelowFunc(reference); if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false }; if (guildRef.current !== guild) return { hasMoreBelow: false, removedFromTop: false }; setFetchBelowError(null); if (belowElements) { const hasMoreBelow = belowElements.length >= maxFetchElements; let removedFromTop = false; setValue(currentValue => { let newValue = (currentValue ?? []).concat(belowElements).sort(sortFunc); if (newValue.length > maxElements) { newValue = newValue.slice(Math.max(newValue.length - maxElements, 0)); removedFromTop = true; } return newValue; }); return { hasMoreBelow, removedFromTop }; } else { return { hasMoreBelow: false, removedFromTop: false }; } } catch (e: unknown) { LOG.error('error fetching below for subscription', e); if (!isMounted.current) return { hasMoreBelow: false, removedFromTop: false }; if (guildRef.current !== guild) return { hasMoreBelow: false, removedFromTop: false }; setFetchBelowError(e); return { hasMoreBelow: true, removedFromTop: false }; } }, [ value, fetchBelowFunc, maxFetchElements ]); const onFetch = useCallback((fetchValue: T[] | null, fetchValueGuild: CombinedGuild) => { let hasMoreAbove = false; if (fetchValue) { if (fetchValue.length >= maxFetchElements) hasMoreAbove = true; fetchValue = fetchValue.slice(Math.max(fetchValue.length - maxElements)).sort(sortFunc); } setFetchResult({ hasMoreAbove, hasMoreBelow: false }); setValue(fetchValue); setValueGuild(fetchValueGuild); setFetchError(null); events.emit('fetch'); }, [ sortFunc, maxFetchElements, maxElements ]); const onFetchError = useCallback((e: unknown) => { setFetchError(e); setValue(null); setValueGuild(null); events.emit('fetch-error'); }, []); const onNew = useCallback((newElements: T[], newElementsGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; let newValue = currentValue.concat(newElements).sort(sortFunc); if (newValue.length > maxElements) { newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements)); } return newValue; }); if (newElementsGuild !== guildRef.current) { LOG.warn(`new elements guild (${newElementsGuild.id}) != current guild (${guildRef.current})`); } events.emit('new', newElements); }, [ sortFunc, getRemoveCounts ]); const onUpdated = useCallback((updatedElements: T[], updatedElementsGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; return currentValue.map(element => updatedElements.find(updatedElement => updatedElement.id === element.id) ?? element).sort(sortFunc); }); if (updatedElementsGuild !== guildRef.current) { LOG.warn(`updated elements guild (${updatedElementsGuild.id}) != current guild (${guildRef.current})`); } events.emit('updated', updatedElements); }, [ sortFunc ]); const onRemoved = useCallback((removedElements: T[], removedElementsGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; const deletedIds = new Set(removedElements.map(deletedElement => deletedElement.id)); return currentValue.filter(element => !deletedIds.has(element.id)).sort(sortFunc); }); if (removedElementsGuild !== guildRef.current) { LOG.warn(`updated elements guild (${removedElementsGuild.id}) != current guild (${guildRef.current})`); } events.emit('removed', removedElements); }, [ sortFunc ]); const onConflict = useCallback((changes: Changes, changesGuild: CombinedGuild) => { setValue(currentValue => { if (currentValue === null) return null; const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)); let newValue = currentValue .concat(changes.added) .map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element) .filter(element => !deletedIds.has(element.id)) .sort(sortFunc); if (newValue.length > maxElements) { newValue = removeByCounts(newValue, getRemoveCounts(newValue.length - maxElements)); } return newValue; }); if (changesGuild !== guildRef.current) { LOG.warn(`conflict changes guild (${changesGuild.id}) != current guild (${guildRef.current})`); } events.emit('conflict', changes); }, [ sortFunc, getRemoveCounts ]); // 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 const boundNewFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; // Cancel calls when the guild changes onNew(newEventArgsMap(...args), guild); }, [ guild, onNew, newEventArgsMap ]) as (Connectable & Conflictable)[NE]; const boundUpdateFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; // Cancel calls when the guild changes onUpdated(updatedEventArgsMap(...args), guild); }, [ onUpdated, updatedEventArgsMap ]) as (Connectable & Conflictable)[UE]; const boundRemovedFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; // Cancel calls when the guild changes onRemoved(removedEventArgsMap(...args), guild); }, [ onRemoved, removedEventArgsMap ]) as (Connectable & Conflictable)[RE]; const boundConflictFunc = useCallback((...args: Arguments): void => { if (!isMounted.current) return; if (guildRef.current !== guild) return; // Cancel calls when the guild changes onConflict(conflictEventArgsMap(...args), guild); }, [ onConflict, conflictEventArgsMap ]) as (Connectable & Conflictable)[CE]; const bindEventsFunc = useCallback(() => { guild.on(newEventName, boundNewFunc); guild.on(updatedEventName, boundUpdateFunc); guild.on(removedEventName, boundRemovedFunc); guild.on(conflictEventName, boundConflictFunc); }, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]); const unbindEventsFunc = useCallback(() => { guild.off(newEventName, boundNewFunc); guild.off(updatedEventName, boundUpdateFunc); guild.off(removedEventName, boundRemovedFunc); guild.off(conflictEventName, boundConflictFunc); }, [ boundNewFunc, boundUpdateFunc, boundRemovedFunc, boundConflictFunc ]); const [ fetchRetryCallable ] = useGuildSubscriptionEffect({ guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc }, fetchFunc); return [ fetchRetryCallable, fetchAboveCallable, fetchBelowCallable, setScrollRatio, fetchResult, value, valueGuild, fetchError, fetchAboveError, fetchBelowError, events ]; } export function useGuildMetadataSubscription(guild: CombinedGuild) { const fetchMetadataFunc = useCallback(async () => { //LOG.silly('fetching metadata for subscription'); return await guild.fetchMetadata(); }, [ guild ]); return useSingleGuildSubscription(guild, { updatedEventName: 'update-metadata', updatedEventArgsMap: (guildMeta: GuildMetadata) => guildMeta, conflictEventName: 'conflict-metadata', conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta }, fetchMetadataFunc); } export function useResourceSubscription(guild: CombinedGuild, resourceId: string | null) { const fetchResourceFunc = useCallback(async () => { //LOG.silly('fetching resource for subscription (resourceId: ' + resourceId + ')'); if (resourceId === null) return null; return await guild.fetchResource(resourceId); }, [ guild, resourceId ]); return useSingleGuildSubscription(guild, { updatedEventName: 'update-resource', updatedEventArgsMap: (resource: Resource) => resource, conflictEventName: 'conflict-resource', conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource }, fetchResourceFunc); } export function useSoftImageSrcResourceSubscription(guild: CombinedGuild, resourceId: string | null): [ imgSrc: string, resource: Resource | null, resourceGuild: CombinedGuild | null, fetchError: unknown | null ] { const [ resource, resourceGuild, fetchError ] = useResourceSubscription(guild, resourceId); const [ imgSrc ] = useOneTimeAsyncAction( async () => { if (fetchError) return './img/error.png'; if (!resource) return './img/loading.svg'; return await ElementsUtil.getImageSrcFromBufferFailSoftly(resource.data); }, './img/loading.svg', [ resource, fetchError ] ); return [ imgSrc, resource, resourceGuild, fetchError ]; } export function useChannelsSubscription(guild: CombinedGuild) { const fetchChannelsFunc = useCallback(async () => { return await guild.fetchChannels(); }, [ guild ]); return useMultipleGuildSubscription(guild, { newEventName: 'new-channels', newEventArgsMap: (channels: Channel[]) => channels, updatedEventName: 'update-channels', updatedEventArgsMap: (updatedChannels: Channel[]) => updatedChannels, removedEventName: 'remove-channels', removedEventArgsMap: (removedChannels: Channel[]) => removedChannels, conflictEventName: 'conflict-channels', conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes) => changes, sortFunc: Channel.sortByIndex }, fetchChannelsFunc); } export function useMembersSubscription(guild: CombinedGuild) { const fetchMembersFunc = useCallback(async () => { return await guild.fetchMembers(); }, [ guild ]); return useMultipleGuildSubscription(guild, { newEventName: 'new-members', newEventArgsMap: (members: Member[]) => members, updatedEventName: 'update-members', updatedEventArgsMap: (updatedMembers: Member[]) => updatedMembers, removedEventName: 'remove-members', removedEventArgsMap: (removedMembers: Member[]) => removedMembers, conflictEventName: 'conflict-members', conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes) => changes, sortFunc: Member.sortForList }, fetchMembersFunc); } export function useSelfMemberSubscription(guild: CombinedGuild): [ selfMember: Member | null, selfMemberGuild: CombinedGuild | null ] { const [ fetchRetryCallable, members, membersGuild, fetchError ] = useMembersSubscription(guild); // TODO: Show an error if we can't fetch and allow retry const selfMember = useMemo(() => { if (members) { const member = members.find(m => m.id === guild.memberId); if (!member) { LOG.warn('Unable to find self in members'); return null; } return member; } return null; }, [ guild.memberId, members ]); return [ selfMember, membersGuild ]; } export function useTokensSubscription(guild: CombinedGuild) { const fetchTokensFunc = useCallback(async () => { //LOG.silly('fetching tokens for subscription'); return await guild.fetchTokens(); }, [ guild ]); return useMultipleGuildSubscription(guild, { newEventName: 'new-tokens', newEventArgsMap: (tokens: Token[]) => tokens, updatedEventName: 'update-tokens', updatedEventArgsMap: (updatedTokens: Token[]) => updatedTokens, removedEventName: 'remove-tokens', removedEventArgsMap: (removedTokens: Token[]) => removedTokens, conflictEventName: 'conflict-tokens', conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes) => changes, sortFunc: Token.sortRecentCreatedFirst }, fetchTokensFunc); } export function useMessagesScrollingSubscription(guild: CombinedGuild, channel: Channel) { const maxFetchElements = Globals.MESSAGES_PER_REQUEST; const maxElements = Globals.MAX_CURRENT_MESSAGES; const fetchMessagesFunc = useCallback(async () => { return await guild.fetchMessagesRecent(channel.id, maxFetchElements); }, [ guild, channel.id, maxFetchElements ]); const fetchAboveFunc = useCallback(async (reference: Message) => { return await guild.fetchMessagesBefore(channel.id, reference.id, maxFetchElements); }, [ guild, channel.id, maxFetchElements ]); const fetchBelowFunc = useCallback(async (reference: Message) => { return await guild.fetchMessagesAfter(channel.id, reference.id, maxFetchElements); }, [ guild, channel.id, maxFetchElements ]); return useMultipleGuildSubscriptionScrolling( guild, { newEventName: 'new-messages', newEventArgsMap: (messages: Message[]) => messages, updatedEventName: 'update-messages', updatedEventArgsMap: (updatedMessages) => updatedMessages, removedEventName: 'remove-messages', removedEventArgsMap: (removedMessages) => removedMessages, conflictEventName: 'conflict-messages', conflictEventArgsMap: (query: PartialMessageListQuery, changesType: AutoVerifierChangesType, changes: Changes) => changes, sortFunc: Message.sortOrder }, maxElements, maxFetchElements, fetchMessagesFunc, fetchAboveFunc, fetchBelowFunc ) }