212 lines
8.8 KiB
TypeScript
212 lines
8.8 KiB
TypeScript
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 { 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<Set<number>>(new Set<number>());
|
|
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<GuildMetadata>) => {
|
|
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<CombinedGuild, {
|
|
updateCallback: (guildMeta: GuildMetadata) => 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<T extends { id: string }>(src: T[], changes: Changes<T>) {
|
|
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<string | null>(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<Member>) => {
|
|
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<GuildMetadata>).guild.id);
|
|
}
|
|
}, [ guilds, selectedGuildId ]);
|
|
|
|
// Automatically load members for the selected guild
|
|
useRecoilSelectedGuildMembers();
|
|
}
|
|
|