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 { ReactNode } from 'react'; import { atom, selector } from 'recoil'; import { Channel, GuildMetadata, Member, Message } from '../../data-types'; import CombinedGuild from '../../guild-combined'; import GuildsManager from '../../guilds-manager'; export type WithUnloadedValue = { value: undefined; hasValueError: false; valueError: undefined; }; export type WithLoadedValue = T extends undefined ? never : { value: T; hasValueError: false; valueError: undefined; }; export type WithFailedValue = { value: undefined; hasValueError: true; valueError: unknown; }; export type WithLoadableValue = WithUnloadedValue | WithLoadedValue | WithFailedValue; // NOTE: Should not extend this type. It is for type guards export type GuildWithValue = WithLoadedValue & { guild: CombinedGuild }; export type GuildWithErrorableValue = WithLoadableValue & { guild: CombinedGuild }; export type ChannelWithErrorableValue = WithLoadableValue & { channel: Channel }; // Devoid is when we don't have a selected guild yet. export function isDevoid(state: NonNullable | null): state is null { return state === null; } // NOTE: Using "pended" instead of "pending" since it has 6 letters like the rest of these functions. It will make the code look nicer :) export function isPended(withLoadableValue: WithLoadableValue): withLoadableValue is WithUnloadedValue { return withLoadableValue.value === undefined && withLoadableValue.hasValueError === false; } export function isLoaded(withLoadableValue: WithLoadableValue): withLoadableValue is WithLoadedValue { return withLoadableValue.value !== undefined; } export function isFailed(withLoadableValue: WithLoadableValue): withLoadableValue is WithFailedValue { return withLoadableValue.hasValueError; } // WARNING: This must NOT be used in a place that could prevent hooks from running // NOTE: This is useful to skip rendering elements that require an atom if its value is not available yet. Using this in the parent element will prevent the child eleemnt from // needing to implement the complexity of loading without a value export function isDevoidPendedOrFailed(withNullableLoadableValue: WithLoadableValue | null): withNullableLoadableValue is null | WithUnloadedValue | WithFailedValue { return withNullableLoadableValue === null || isPended(withNullableLoadableValue) || isFailed(withNullableLoadableValue); } export const overlayState = atom({ key: 'overlayState', default: null }); export const guildsManagerState = atom({ key: 'guildsManagerState', default: null, // Allow mutability so that we can have changes to internal guild stuff // We will still listen to new/update/delete/conflict events to maintain the UI's state dangerouslyAllowMutability: true }); export const guildsState = atom[] | null>({ key: 'guildsState', default: null, dangerouslyAllowMutability: true }); export const selectedGuildIdState = atom({ key: 'selectedGuildIdState', default: null }); export const selectedGuildWithMetaState = selector | null>({ key: 'selectedGuildWithMetaState', get: ({ get }) => { const guildsWithMeta = get(guildsState); if (guildsWithMeta === null) return null; const guildId = get(selectedGuildIdState); if (guildId === null) return null; return guildsWithMeta.find(guildWithMeta => guildWithMeta.guild.id === guildId) ?? null; }, dangerouslyAllowMutability: true }); // Note: You likely want selectedGuildWithMetaState export const selectedGuildState = selector({ key: 'selectedGuildState', get: ({ get }) => { const guildWithMeta = get(selectedGuildWithMetaState); if (isDevoid(guildWithMeta)) return null; return guildWithMeta.guild; }, dangerouslyAllowMutability: true }); export const selectedGuildWithMembersState = atom | null>({ key: 'selectedGuildWithMembersState', default: null, dangerouslyAllowMutability: true }); export const selectedGuildWithSelfMemberState = selector | null>({ key: 'selectedGuildWithSelfMemberState', get: ({ get }) => { const guildWithMembers = get(selectedGuildWithMembersState); if (isDevoid(guildWithMembers)) return null; if (isFailed(guildWithMembers)) return { guild: guildWithMembers.guild, value: undefined, hasValueError: true, valueError: 'members fetch error' }; if (isPended(guildWithMembers)) return { guild: guildWithMembers.guild, value: undefined, hasValueError: false, valueError: undefined }; const selfMember = guildWithMembers.value.find(member => member.id === guildWithMembers.guild.memberId) ?? null; if (selfMember === null) { LOG.warn('unable to find self member in members'); return { guild: guildWithMembers.guild, value: undefined, hasValueError: true, valueError: 'unable to find self in members' }; } return { guild: guildWithMembers.guild, value: selfMember, hasValueError: false, valueError: undefined }; }, dangerouslyAllowMutability: true }); export const selectedGuildWithChannelsState = atom | null>({ key: 'selectedGuildChannelsState', default: null, dangerouslyAllowMutability: true }); export const selectedGuildWithActiveChannelIdState = atom | null>({ key: 'selectedGuildWithActiveChannelIdState', default: null, dangerouslyAllowMutability: true }); export const selectedGuildWithActiveChannelMessagesState = atom> | null>({ key: 'selectedGuildWithActiveChannelMessagesState', default: null, dangerouslyAllowMutability: true }); // TODO: selectedGuildWithActiveChannelState