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 { useCallback, useEffect, useRef } from "react"; import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; import { GuildMetadata } from "../../data-types"; import GuildsManager from "../../guilds-manager"; import { guildsManagerState, guildsState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState } from "./atoms"; import { useIsMountedRef } from "./react-helper"; import CombinedGuild from '../../guild-combined'; import { AutoVerifierChangesType } from '../../auto-verifier'; /** Manages the guildsManager to ensure that guildsState is up to date with a list of guilds * and that the guilds attempt to fetch their corresponding guildMetas * This class it a bit more complicated since it has to fetch/listen to guild metadata changes on many guilds at once */ function useManagedGuildsManager(guildsManager: GuildsManager) { const isMounted = useIsMountedRef(); const metadataInProgress = useRef>(new Set()); const [ guildsWithMeta, setGuildsWithMeta ] = useRecoilState(guildsState); // Update the guild list // If new guilds are added, add them with null metadata const onChange = useCallback(() => { const guilds = guildsManager.guilds; setGuildsWithMeta((prevGuildsWithMeta) => { if (!prevGuildsWithMeta) { return guilds.map(guild => ({ guild, value: null, valueError: null })); } else { const newGuilds = guilds.filter(guild => !prevGuildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guild.id)); const newGuildsWithMeta = prevGuildsWithMeta .filter(guildWithMeta => guilds.find(guild => guildWithMeta.guild.id === guild.id)) // Remove old guilds .concat(newGuilds.map(guild => ({ guild, value: null, valueError: null }))); // Add new guilds return newGuildsWithMeta; } }); }, [ guildsManager, setGuildsWithMeta ]); // Listen for changes to the guild manager useEffect(() => { // Set initial guilds onChange(); // Listen for updates guildsManager.on('update-guilds', onChange); return () => { guildsManager.off('update-guilds', onChange); } }, [ guildsManager, onChange ]); const setGuildWithMeta = useCallback((guildId: number, newGuildWithMeta: GuildWithErrorableValue) => { setGuildsWithMeta(prevGuildsWithMeta => { if (!prevGuildsWithMeta) return null; return prevGuildsWithMeta.map(guildWithMeta => guildWithMeta.guild.id === guildId ? newGuildWithMeta : guildWithMeta); }); }, [ setGuildsWithMeta ]); useEffect(() => { if (guildsWithMeta === null) return; // Load guild metadatas for (const { guild, value, valueError } of guildsWithMeta) { // Only fetch if the value has not been fetched yet and we are not currently fetching it. if (!metadataInProgress.current.has(guild.id) && value === null && valueError === null) { (async () => { metadataInProgress.current.add(guild.id); try { const guildMeta = await guild.fetchMetadata(); if (!isMounted.current) return; setGuildWithMeta(guild.id, { guild, value: guildMeta, valueError: null }); } catch (e: unknown) { LOG.error(`error fetching metadata for g#${guild.id}`, e); if (!isMounted.current) return; setGuildWithMeta(guild.id, { guild, value: null, valueError: e }); } metadataInProgress.current.delete(guild.id); })(); } } }, [ guildsWithMeta, setGuildWithMeta ]); // Listen for changes to the guild metadata useEffect(() => { if (guildsWithMeta === null) return; // Listen for changes to metadata function handleUpdateOrConflict(guild: CombinedGuild, guildMeta: GuildMetadata) { setGuildWithMeta(guild.id, { guild, value: guildMeta, valueError: null }); } const callbacks = new Map void, conflictCallback: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => void }>(); for (const { guild } of guildsWithMeta) { const updateCallback = (guildMeta: GuildMetadata) => handleUpdateOrConflict(guild, guildMeta); const conflictCallback = (_changesType: AutoVerifierChangesType, _oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => handleUpdateOrConflict(guild, guildMeta); guild.on('update-metadata', updateCallback); guild.on('conflict-metadata', conflictCallback); callbacks.set(guild, { updateCallback, conflictCallback }); } return () => { for (const { guild } of guildsWithMeta) { const { updateCallback, conflictCallback } = callbacks.get(guild) as { updateCallback: (guildMeta: GuildMetadata) => void, conflictCallback: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, guildMeta: GuildMetadata) => void }; guild.off('update-metadata', updateCallback); guild.off('conflict-metadata', conflictCallback); } } }, [ guildsWithMeta ]); //useEffect(() => { // LOG.debug(`guildsWithMeta: `, { guildsWithMeta }); //}, [ guildsWithMeta ]); } // Entrypoint for recoil setup with guilds export function useGuildsManagerWithRecoil(guildsManager: GuildsManager): void { const setGuildsManager = useSetRecoilState(guildsManagerState); const guilds = useRecoilValue(guildsState); const [ selectedGuildId, setSelectedGuildId ] = useRecoilState(selectedGuildIdState); // Set the guilds manager atom useEffect(() => { setGuildsManager(guildsManager); }, [ guildsManager, setGuildsManager ]); // Manage the guilds within the manager (adds them to the guilds atom and automatically loads and updates their metadata) useManagedGuildsManager(guildsManager); // Make sure that the first guild is set to the active guild once we get some guilds useEffect(() => { if (guilds && guilds.length > 0 && selectedGuildId === null) { setSelectedGuildId((guilds[0] as GuildWithErrorableValue).guild.id); } }); }