recoilize members
This commit is contained in:
parent
245637bed6
commit
0609aa687c
@ -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);
|
||||
|
||||
|
@ -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' }
|
||||
|
@ -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(() => {
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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 ]);
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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}>
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user