import * as electronRemote from '@electron/remote'; const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger'; const LOG = Logger.create(__filename, electronConsole); import { useCallback, useEffect, useRef } from "react"; import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; import { Changes, GuildMetadata, Member } from "../../data-types"; import GuildsManager from "../../guilds-manager"; import { guildsManagerState, guildsState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState, selectedGuildWithMembersState } from "./atoms"; import { useIsMountedRef } from "./react-helper"; import CombinedGuild from '../../guild-combined'; import { AutoVerifierChangesType } from '../../auto-verifier'; import * as uuid from 'uuid'; /** 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 === null) { return guilds.map(guild => ({ guild, value: undefined, hasValueError: false, valueError: undefined })); } 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: undefined, hasValueError: false, valueError: undefined }))); // 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 // TODO: How do we add a retry for guild metadata? It's used in a lot of places! for (const { guild, value, hasValueError } 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 === undefined && hasValueError === false) { (async () => { metadataInProgress.current.add(guild.id); try { const guildMeta = await guild.fetchMetadata(); if (!isMounted.current) return; LOG.debug('loaded metadata for g#' + guild.id); setGuildWithMeta(guild.id, { guild, value: guildMeta, hasValueError: false, valueError: undefined }); } catch (e: unknown) { LOG.error(`error fetching metadata for g#${guild.id}`, e); if (!isMounted.current) return; setGuildWithMeta(guild.id, { guild, value: undefined, hasValueError: true, 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, hasValueError: false, valueError: undefined }); } 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 ]); } function applyChanges(src: T[], changes: Changes) { const deletedIds = new Set(changes.deleted.map(deletedElement => deletedElement.id)) return src.concat(changes.added) .map(element => changes.updated.find(change => change.newDataPoint.id === element.id)?.newDataPoint ?? element) .filter(element => !deletedIds.has(element.id)); } export function useRecoilSelectedGuildMembers(): void { const isMounted = useIsMountedRef(); const guild = useRecoilValue(selectedGuildState); const setGuildWithMembers = useSetRecoilState(selectedGuildWithMembersState); const lastFetchId = useRef(null); // TODO: Test this custom effect out // Fetch initial value useEffect(() => { if (guild === null) return; (async () => { // TODO: Set value to null if the fetch takes > 300 ms const fetchId = uuid.v4(); try { lastFetchId.current = fetchId; const members = await guild.fetchMembers(); if (!isMounted.current) return; if (lastFetchId.current !== fetchId) return; // Another fetch happened while we were fetching setGuildWithMembers({ guild, value: members, hasValueError: false, valueError: undefined }); } catch (e: unknown) { LOG.error('unable to perform members fetch', e); if (!isMounted.current) return; if (lastFetchId.current !== fetchId) return; // Another fetch happened while we were fetching setGuildWithMembers({ guild, value: undefined, hasValueError: true, valueError: e }); } })(); }, [ guild, isMounted ]); // Listen for Changes useEffect(() => { if (guild === null) return; const updateCallback = (updatedMembers: Member[]) => setGuildWithMembers({ guild, value: updatedMembers, hasValueError: false, valueError: undefined }); const conflictCallback = (_changesType: AutoVerifierChangesType, changes: Changes) => { setGuildWithMembers(prevGuildWithMembers => { if (prevGuildWithMembers === null) return null; if (prevGuildWithMembers.valueError) return prevGuildWithMembers; return { guild, value: applyChanges(prevGuildWithMembers.value ?? [], changes), hasValueError: false, valueError: undefined }; }); }; guild.on('update-members', updateCallback); guild.on('conflict-members', conflictCallback); return () => { guild.off('update-members', updateCallback); guild.off('conflict-members', conflictCallback); } }, [ guild ]); } // 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); } }, [ guilds, selectedGuildId ]); // Automatically load members for the selected guild useRecoilSelectedGuildMembers(); }