recoilize members

This commit is contained in:
Michael Peters 2022-01-29 17:05:12 -06:00
parent 245637bed6
commit 0609aa687c
14 changed files with 270 additions and 152 deletions

View File

@ -1,10 +1,9 @@
import moment from 'moment';
import React, { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { Member, Token } from '../../data-types';
import { GuildMetadata, Member, Token } from '../../data-types';
import Util from '../../util';
import { IAddGuildData } from '../overlays/overlay-add-guild';
import { selectedGuildWithMetaState } from '../require/atoms';
import { GuildWithValue } from '../require/atoms';
import BaseElements from '../require/base-elements';
import { useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import { useAsyncVoidCallback, useDownloadButton, useOneTimeAsyncAction } from '../require/react-helper';
@ -13,17 +12,18 @@ import Button, { ButtonColorType } from './button';
export interface TokenRowProps {
url: string;
token: Token;
guildWithMeta: GuildWithValue<GuildMetadata>
}
const TokenRow: FC<TokenRowProps> = (props: TokenRowProps) => {
const { token } = props;
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState);
if (guildWithMeta === null || guildWithMeta.value === null) return null;
const { token, guildWithMeta } = props;
const [ guildSocketConfigs ] = useOneTimeAsyncAction(
async () => await guildWithMeta.guild.fetchSocketConfigs(),
async () => {
return await guildWithMeta.guild.fetchSocketConfigs()
},
null,
[ guildWithMeta.guild ]
[ guildWithMeta ]
);
const [ iconSrc ] = useSoftImageSrcResourceSubscription(guildWithMeta.guild, guildWithMeta.value.iconResourceId, guildWithMeta.guild);

View File

@ -1,29 +1,26 @@
import React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { overlayState } from '../require/atoms';
import { GuildMetadata, Member } from '../../data-types';
import { GuildWithValue, overlayState } from '../require/atoms';
import PersonalizeOverlay from '../overlays/overlay-personalize';
import { SubscriptionResult } from '../require/guild-subscriptions';
import ContextMenu from './components/context-menu';
export interface ConnectionInfoContextMenuProps {
guild: CombinedGuild;
selfMemberResult: SubscriptionResult<Member>;
guildWithSelfMember: GuildWithValue<Member>;
relativeToRef: RefObject<HTMLElement>;
close: () => void;
}
const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: ConnectionInfoContextMenuProps) => {
const { guild, selfMemberResult, relativeToRef, close } = props;
const { guildWithSelfMember, relativeToRef, close } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState)
const setSelfStatus = useCallback(async (status: string) => {
if (selfMemberResult.value.status !== status) {
await guild.requestSetStatus(status);
if (guildWithSelfMember.value.status !== status) {
await guildWithSelfMember.guild.requestSetStatus(status);
}
}, [ guild, selfMemberResult ]);
}, [ guildWithSelfMember ]);
const statusElements = useMemo(() => {
return [ 'online', 'away', 'busy', 'invisible' ].map(status => {
@ -39,8 +36,8 @@ const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: Co
const openPersonalize = useCallback(() => {
close();
setOverlay(<PersonalizeOverlay guild={guild} selfMemberResult={selfMemberResult} />);
}, [ guild, selfMemberResult, close ]);
setOverlay(<PersonalizeOverlay guildWithSelfMember={guildWithSelfMember} />);
}, [ close ]);
const alignment = useMemo(() => {
return { bottom: 'top', centerX: 'centerX' }

View File

@ -1,7 +1,12 @@
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 React, { FC, ReactNode, RefObject, useCallback, useMemo } from 'react';
import { useSetRecoilState } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Member } from '../../data-types';
import { overlayState } from '../require/atoms';
import { isDevoidPendedOrFailed, overlayState, selectedGuildWithMetaState } from '../require/atoms';
import ChannelOverlay from '../overlays/overlay-channel';
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
import BaseElements from '../require/base-elements';
@ -16,11 +21,17 @@ export interface GuildTitleContextMenuProps {
const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitleContextMenuProps) => {
const { close, relativeToRef, selfMember } = props;
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState);
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const openGuildSettings = useCallback(() => {
close();
setOverlay(<GuildSettingsOverlay />);
if (isDevoidPendedOrFailed(guildWithMeta)) {
LOG.warn('not opening guild settings context menu due to devoid/pended/failed guildWithMeta');
return;
}
setOverlay(<GuildSettingsOverlay guildWithMeta={guildWithMeta} />);
}, [ close ]);
const openCreateChannel = useCallback(() => {

View File

@ -6,7 +6,7 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useEffect, useMemo, useState } from 'react';
import Display from '../components/display';
import InvitePreview from '../components/invite-preview';
import { Token } from '../../data-types';
import { GuildMetadata, Token } from '../../data-types';
import { useAsyncSubmitButton } from '../require/react-helper';
import { Duration } from 'moment';
import moment from 'moment';
@ -14,12 +14,13 @@ import DropdownInput from '../components/input-dropdown';
import Button from '../components/button';
import { useTokensSubscription, useSoftImageSrcResourceSubscription } from '../require/guild-subscriptions';
import TokenRow from '../components/token-row';
import { useRecoilValue } from 'recoil';
import { selectedGuildWithMetaState } from '../require/atoms';
import { GuildWithValue } from '../require/atoms';
const GuildInvitesDisplay: FC = () => {
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState);
if (guildWithMeta === null || guildWithMeta.value === null) return null;
export interface GuildInvitesDisplayProps {
guildWithMeta: GuildWithValue<GuildMetadata>;
}
const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDisplayProps) => {
const { guildWithMeta } = props;
const url = 'https://localhost:3030'; // TODO: this will likely be a dropdown list at some point
@ -67,7 +68,7 @@ const GuildInvitesDisplay: FC = () => {
if (!guildWithMeta?.value) {
return <div className="no-guild-meta">No Guild Metadata</div>;
}
return tokensResult?.value?.map((token: Token) => <TokenRow key={guildWithMeta.guild.id + token.token} url={url} token={token} />);
return tokensResult?.value?.map((token: Token) => <TokenRow key={guildWithMeta.guild.id + token.token} url={url} token={token} guildWithMeta={guildWithMeta} />);
}, [ url, guildWithMeta, tokensResult, tokensError ]);
return (

View File

@ -10,13 +10,14 @@ import Display from '../components/display';
import TextInput from '../components/input-text';
import ImageEditInput from '../components/input-image-edit';
import { useResourceSubscription } from '../require/guild-subscriptions';
import { useRecoilValue } from 'recoil';
import { selectedGuildWithMetaState } from '../require/atoms';
import { GuildWithValue } from '../require/atoms';
import { GuildMetadata } from '../../data-types';
const GuildOverviewDisplay: FC = () => {
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState);
if (guildWithMeta === null || guildWithMeta.value === null) return null;
interface GuildOverviewDisplayProps {
guildWithMeta: GuildWithValue<GuildMetadata>;
}
const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOverviewDisplayProps) => {
const { guildWithMeta } = props;
// TODO: Use the one from guild.tsx (for both of these?)
const [ iconResourceResult, iconResourceError ] = useResourceSubscription(guildWithMeta.guild, guildWithMeta.value.iconResourceId, guildWithMeta.guild);

View File

@ -15,13 +15,13 @@ export interface DummyMember {
export interface MemberProps {
guild: CombinedGuild;
member: Member | DummyMember;
memberGuild: CombinedGuild | null;
}
const MemberElement: FC<MemberProps> = (props: MemberProps) => {
const { guild, member, memberGuild } = props;
const { guild, member } = props;
const [ avatarSrc ] = useSoftImageSrcResourceSubscription(guild, member.avatarResourceId, memberGuild);
// TODO: This is a terrible line of code. Make sure to fix it whe we do resources in recoil
const [ avatarSrc ] = useSoftImageSrcResourceSubscription(guild, member.avatarResourceId, guild);
const nameStyle = useMemo(() => member.roleColor ? { color: member.roleColor } : {}, [ member.roleColor ]);

View File

@ -5,30 +5,20 @@ const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useMemo } from 'react';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subscriptions';
import MemberElement from './components/member-element';
import { useRecoilValue } from 'recoil';
import { isDevoid, isPended, isFailed, selectedGuildWithMembersState } from '../require/atoms';
export interface MemberListProps {
guild: CombinedGuild;
membersResult: SubscriptionResult<Member[] | null> | null;
membersFetchError: unknown | null;
}
const MemberList: FC<MemberListProps> = (props: MemberListProps) => {
const { guild, membersResult, membersFetchError } = props;
const MemberList: FC = () => {
const guildWithMembers = useRecoilValue(selectedGuildWithMembersState);
const memberElements = useMemo(() => {
if (membersFetchError) {
// TODO: Try Again
return <div className="members-failed">Unable to load members</div>
}
if (!isNonNullAndHasValue(membersResult)) {
return <div className="members-loading">Loading members...</div>
}
if (isDevoid(guildWithMembers)) return null;
if (isPended(guildWithMembers)) return <div className="members-loading">Loading Members...</div>;
if (isFailed(guildWithMembers)) return <div className="members-failed">Unable to load members</div>;
//LOG.debug(`drawing ${membersResult.value.length} members`);
return membersResult.value.map((member: Member) => <MemberElement key={guild.id + member.id} guild={guild} member={member} memberGuild={membersResult.guild} />);
}, [ guild, membersResult, membersFetchError ]);
return guildWithMembers.value.map((member: Member) => <MemberElement key={guildWithMembers.guild.id + member.id} guild={guildWithMembers.guild} member={member} />);
}, [ guildWithMembers ]);
return (
<div className="member-list">

View File

@ -3,13 +3,14 @@ import ChoicesControl from "../components/control-choices";
import GuildInvitesDisplay from "../displays/display-guild-invites";
import GuildOverviewDisplay from "../displays/display-guild-overview";
import Overlay from '../components/overlay';
import { useRecoilValue } from "recoil";
import { selectedGuildWithMetaState } from "../require/atoms";
import { GuildWithValue } from "../require/atoms";
import { GuildMetadata } from "../../data-types";
const GuildSettingsOverlay: FC = () => {
const guildWithMeta = useRecoilValue(selectedGuildWithMetaState);
if (guildWithMeta === null || guildWithMeta.value === null) return null;
interface GuildSettingsOverlayProps {
guildWithMeta: GuildWithValue<GuildMetadata>;
}
const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
const { guildWithMeta } = props;
const rootRef = useRef<HTMLDivElement>(null);
@ -17,10 +18,10 @@ const GuildSettingsOverlay: FC = () => {
const [ display, setDisplay ] = useState<JSX.Element>();
useEffect(() => {
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay />);
if (selectedId === 'overview') setDisplay(<GuildOverviewDisplay guildWithMeta={guildWithMeta} />);
//if (selectedId === 'roles' ) setDisplay(<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />);
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay />);
}, [ selectedId ]);
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guildWithMeta={guildWithMeta} />);
}, [ selectedId, guildWithMeta, setDisplay ]);
return (
<Overlay childRootRef={rootRef}>

View File

@ -4,38 +4,38 @@ import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { createRef, FC, MutableRefObject, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Member } from '../../data-types';
import Globals from '../../globals';
import CombinedGuild from '../../guild-combined';
import ImageEditInput from '../components/input-image-edit';
import TextInput from '../components/input-text';
import SubmitOverlayLower from '../components/submit-overlay-lower';
import { useAsyncSubmitButton } from '../require/react-helper';
import Button from '../components/button';
import Overlay from '../components/overlay';
import { SubscriptionResult, useResourceSubscription } from '../require/guild-subscriptions';
import { useResourceSubscription } from '../require/guild-subscriptions';
import { useSetRecoilState } from 'recoil';
import { overlayState } from '../require/atoms';
import { overlayState, GuildWithValue } from '../require/atoms';
import { Member } from '../../data-types';
export interface PersonalizeOverlayProps {
guild: CombinedGuild;
selfMemberResult: SubscriptionResult<Member>;
interface PersonalizeOverlayProps {
guildWithSelfMember: GuildWithValue<Member>;
}
const PersonalizeOverlay: FC<PersonalizeOverlayProps> = (props: PersonalizeOverlayProps) => {
const { guild, selfMemberResult } = props;
const { guildWithSelfMember } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
const { guild, value: selfMember } = guildWithSelfMember;
const rootRef = useRef<HTMLDivElement>(null);
const [ avatarResourceResult, avatarResourceError ] = useResourceSubscription(guild, selfMemberResult.value.avatarResourceId, selfMemberResult.guild);
const [ avatarResourceResult, avatarResourceError ] = useResourceSubscription(guild, selfMember.avatarResourceId, guild);
const displayNameInputRef = createRef<HTMLInputElement>();
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(selfMemberResult.value.displayName);
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(selfMember.displayName);
const [ savedAvatarBuff, setSavedAvatarBuff ] = useState<Buffer | null>(null);
const [ displayName, setDisplayName ] = useState<string>(selfMemberResult.value.displayName);
const [ displayName, setDisplayName ] = useState<string>(selfMember.displayName);
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
const [ displayNameInputValid, setDisplayNameInputValid ] = useState<boolean>(false);

View File

@ -1,24 +1,57 @@
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, DefaultValue, selector } from 'recoil';
import { atom, selector } from 'recoil';
import { Channel, GuildMetadata, Member, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import GuildsManager from '../../guilds-manager';
export interface GuildWithValue<T> {
guild: CombinedGuild;
export type WithUnloadedValue = {
value: undefined;
hasValueError: false;
valueError: undefined;
};
export type WithLoadedValue<T> = T extends undefined ? never : {
value: T;
hasValueError: false;
valueError: undefined;
};
export type WithFailedValue = {
value: undefined;
hasValueError: true;
valueError: unknown;
};
export type WithLoadableValue<T> = WithUnloadedValue | WithLoadedValue<T> | WithFailedValue;
// NOTE: Should not extend this type. It is for type guards
export type GuildWithValue<T> = WithLoadedValue<T> & { guild: CombinedGuild };
export type GuildWithErrorableValue<T> = WithLoadableValue<T> & { guild: CombinedGuild };
export type ChannelWithErrorableValue<T> = WithLoadableValue<T> & { channel: Channel };
// Devoid is when we don't have a selected guild yet.
export function isDevoid<T>(state: NonNullable<T> | 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<T>(withLoadableValue: WithLoadableValue<T>): withLoadableValue is WithUnloadedValue {
return withLoadableValue.value === undefined && withLoadableValue.hasValueError === false;
}
export function isLoaded<T>(withLoadableValue: WithLoadableValue<T>): withLoadableValue is WithLoadedValue<T> {
return withLoadableValue.value !== undefined;
}
export function isFailed<T>(withLoadableValue: WithLoadableValue<T>): withLoadableValue is WithFailedValue {
return withLoadableValue.hasValueError;
}
export interface GuildWithErrorableValue<T> {
guild: CombinedGuild;
value: T | null;
valueError: unknown | null;
}
export interface ChannelWithErrorableValue<T> {
channel: Channel;
value: T | null;
valueError: unknown | null;
// WARNING: This should 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<T>(withNullableLoadableValue: WithLoadableValue<T> | null): withNullableLoadableValue is null | WithUnloadedValue | WithFailedValue {
return withNullableLoadableValue === null || isPended(withNullableLoadableValue) || isFailed(withNullableLoadableValue);
}
export const overlayState = atom<ReactNode>({
@ -32,7 +65,7 @@ export const guildsManagerState = atom<GuildsManager | 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<GuildWithErrorableValue<GuildMetadata>[] | null>({
key: 'guildsState',
@ -59,45 +92,59 @@ export const selectedGuildWithMetaState = selector<GuildWithErrorableValue<Guild
dangerouslyAllowMutability: true
});
// Note: You likely want selectedGuildWithMetaState
export const selectedGuildState = selector<CombinedGuild | null>({
key: 'selectedGuildState',
get: ({ get }) => {
const guildWithMeta = get(selectedGuildWithMetaState);
if (guildWithMeta === null) return null;
if (isDevoid(guildWithMeta)) return null;
return guildWithMeta.guild;
},
dangerouslyAllowMutability: true
});
export const selectedGuildMembersState = atom<GuildWithErrorableValue<Member[]> | null>({
key: 'selectedGuildMembersState',
default: null
export const selectedGuildWithMembersState = atom<GuildWithErrorableValue<Member[]> | null>({
key: 'selectedGuildWithMembersState',
default: null,
dangerouslyAllowMutability: true
});
export const selectedGuildWithSelfMemberState = selector<GuildWithErrorableValue<Member> | 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<GuildWithErrorableValue<Channel[]> | null>({
key: 'selectedGuildChannelsState',
default: null
default: null,
dangerouslyAllowMutability: true
});
export const selectedGuildWithActiveChannelIdState = atom<GuildWithValue<string | null> | null>({
key: 'selectedGuildWithActiveChannelIdState',
default: null
default: null,
dangerouslyAllowMutability: true
});
export const selectedGuildWithActiveChannelMessagesState = atom<GuildWithValue<ChannelWithErrorableValue<Message[]> | null> | null>({
export const selectedGuildWithActiveChannelMessagesState = atom<GuildWithValue<ChannelWithErrorableValue<Message[]>> | null>({
key: 'selectedGuildWithActiveChannelMessagesState',
default: null
default: null,
dangerouslyAllowMutability: true
});
export const selectedGuildWithActiveChannelState = selector<GuildWithValue<Channel> | null>({
key: 'selectedGuildActiveChannelState',
get: ({ get }) => {
const guildWithChannelMessages = get(selectedGuildWithActiveChannelMessagesState);
if (guildWithChannelMessages === null || guildWithChannelMessages.value === null) return null;
return { guild: guildWithChannelMessages.guild, value: guildWithChannelMessages.value.channel };
}
});
// TODO: selectedGuildWithActiveChannelState

View File

@ -5,13 +5,13 @@ const LOG = Logger.create(__filename, electronConsole);
import { useCallback, useEffect, useRef } from "react";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { GuildMetadata } from "../../data-types";
import { Changes, GuildMetadata, Member } from "../../data-types";
import GuildsManager from "../../guilds-manager";
import { guildsManagerState, guildsState, GuildWithErrorableValue, selectedGuildIdState, selectedGuildState } from "./atoms";
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
@ -26,15 +26,14 @@ function useManagedGuildsManager(guildsManager: GuildsManager) {
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;
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 ]);
@ -62,19 +61,21 @@ function useManagedGuildsManager(guildsManager: GuildsManager) {
if (guildsWithMeta === null) return;
// Load guild metadatas
for (const { guild, value, valueError } of guildsWithMeta) {
// 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 === null && valueError === null) {
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;
setGuildWithMeta(guild.id, { guild, value: guildMeta, valueError: null });
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: null, valueError: e });
setGuildWithMeta(guild.id, { guild, value: undefined, hasValueError: true, valueError: e });
}
metadataInProgress.current.delete(guild.id);
})();
@ -88,7 +89,7 @@ function useManagedGuildsManager(guildsManager: GuildsManager) {
// Listen for changes to metadata
function handleUpdateOrConflict(guild: CombinedGuild, guildMeta: GuildMetadata) {
setGuildWithMeta(guild.id, { guild, value: guildMeta, valueError: null });
setGuildWithMeta(guild.id, { guild, value: guildMeta, hasValueError: false, valueError: undefined });
}
const callbacks = new Map<CombinedGuild, {
updateCallback: (guildMeta: GuildMetadata) => void,
@ -120,6 +121,72 @@ function useManagedGuildsManager(guildsManager: GuildsManager) {
//}, [ 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);
@ -137,6 +204,9 @@ export function useGuildsManagerWithRecoil(guildsManager: GuildsManager): void {
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();
}

View File

@ -1,23 +1,18 @@
import React, { Dispatch, FC, ReactNode, SetStateAction, useMemo, useRef } from 'react';
import React, { FC, useMemo, useRef } from 'react';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import MemberElement, { DummyMember } from '../lists/components/member-element';
import ConnectionInfoContextMenu from '../contexts/context-menu-connection-info';
import { useContextMenu } from '../require/react-helper';
import { isNonNullAndHasValue, SubscriptionResult } from '../require/guild-subscriptions';
export interface ConnectionInfoProps {
guild: CombinedGuild;
selfMemberResult: SubscriptionResult<Member | null> | null;
}
const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) => {
const { guild, selfMemberResult } = props;
import { useRecoilValue } from 'recoil';
import { isDevoid, isPended, isFailed, selectedGuildWithSelfMemberState, isDevoidPendedOrFailed } from '../require/atoms';
const ConnectionInfo: FC = () => {
const rootRef = useRef<HTMLDivElement>(null);
const guildWithSelfMember = useRecoilValue(selectedGuildWithSelfMemberState);
const displayMember = useMemo((): Member | DummyMember => {
if (!isNonNullAndHasValue(selfMemberResult)) {
if (isDevoid(guildWithSelfMember) || isPended(guildWithSelfMember)) {
return {
id: 'dummy',
displayName: 'Connecting...',
@ -26,25 +21,31 @@ const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) =>
avatarResourceId: null
};
}
return selfMemberResult.value;
}, [ selfMemberResult ]);
if (isFailed(guildWithSelfMember)) {
return {
id: 'dummy',
displayName: 'UNKNOWN',
status: 'unknown',
roleColor: null,
avatarResourceId: null
};
}
return guildWithSelfMember.value;
}, [ guildWithSelfMember ]);
const [ contextMenu, toggleContextMenu ] = useContextMenu((close: () => void) => {
if (!isNonNullAndHasValue(selfMemberResult)) return null;
if (isDevoidPendedOrFailed(guildWithSelfMember)) return null;
return (
<ConnectionInfoContextMenu
guild={guild} selfMemberResult={selfMemberResult} relativeToRef={rootRef}
close={close}
/>
<ConnectionInfoContextMenu guildWithSelfMember={guildWithSelfMember} relativeToRef={rootRef} close={close} />
);
}, [ guild, selfMemberResult, rootRef ]);
}, [ guildWithSelfMember ]);
return (
return guildWithSelfMember?.guild ? (
<div ref={rootRef} className="connection-info">
<div onClick={toggleContextMenu}><MemberElement guild={guild} member={displayMember} memberGuild={selfMemberResult?.guild ?? null} /></div>
<div onClick={toggleContextMenu}><MemberElement guild={guildWithSelfMember.guild} member={displayMember} /></div>
{contextMenu}
</div>
);
) : null;
}
export default ConnectionInfo;

View File

@ -28,7 +28,6 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
// TODO: React jump messages to bottom when the current user sent a message
const [ selfMemberResult ] = useSelfMemberSubscription(guild);
const [ membersRetry, membersResult, membersFetchError ] = useMembersSubscription(guild);
const [ channelsRetry, channelsResult, channelsFetchError ] = useChannelsSubscription(guild);
const [ activeChannel, setActiveChannel ] = useState<Channel | null>(null);
@ -88,7 +87,7 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
channels={channelsResult?.value ?? null} channelsFetchError={channelsFetchError}
activeChannel={activeChannel} setActiveChannel={setActiveChannel}
/>
<ConnectionInfo guild={guild} selfMemberResult={selfMemberResult} />
<ConnectionInfo />
</div>
<div className="guild-channel">
<ChannelTitle channel={activeChannel} />
@ -98,7 +97,7 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
{activeChannel && activeChannelGuild && <SendMessage guild={guild} channel={activeChannel} />}
</div>
<div className="member-list-wrapper">
<MemberList guild={guild} membersResult={membersResult} membersFetchError={membersFetchError} />
<MemberList />
</div>
</div>
</div>