infinite scroll with recoil works!

This commit is contained in:
Michael Peters 2022-02-06 19:16:04 -06:00
parent 00c99cefc8
commit fb0981e92b
6 changed files with 45 additions and 85 deletions

View File

View File

@ -48,17 +48,16 @@ interface PreviewImageElementProps {
resourcePreviewId: string;
resourceId: string;
resourceName: string;
resourceIdGuild: CombinedGuild; // TODO: Remove in favor of just one guild
}
const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageElementProps) => {
const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName, resourceIdGuild } = props;
const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName } = props;
const setOverlay = useSetRecoilState<ReactNode>(overlayState);
// TODO: Handle resourceError
const previewResource = useRecoilValue(guildResourceState({ guildId: resourceIdGuild.id, resourceId: resourcePreviewId }));
const previewImgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: resourceIdGuild.id, resourceId: resourcePreviewId }));
const previewResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: resourcePreviewId }));
const previewImgSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: resourcePreviewId }));
const [ contextMenu, onContextMenu ] = useContextClickContextMenu((_alignment: IAlignment, relativeToPos: { x: number, y: number }, close: () => void) => {
if (!isLoaded(previewResource)) return null;
@ -71,7 +70,7 @@ const PreviewImageElement: FC<PreviewImageElementProps> = (props: PreviewImageEl
}, [ previewResource, resourceName ]);
const openImageOverlay = useCallback(() => {
setOverlay(<ImageOverlay guild={resourceIdGuild} resourceId={resourceId} resourceName={resourceName} />);
setOverlay(<ImageOverlay guild={guild} resourceId={resourceId} resourceName={resourceName} />);
}, [ guild, resourceId, resourceName ]);
return (
@ -89,11 +88,10 @@ export interface MessageElementProps {
guild: CombinedGuild;
message: Message;
prevMessage: Message | null;
messageGuild: CombinedGuild;
}
const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) => {
const { guild, message, prevMessage, messageGuild } = props;
const { guild, message, prevMessage } = props;
const className = useMemo(() => {
return message.isContinued(prevMessage) ? 'message-react continued' : 'message-react';
@ -147,7 +145,6 @@ const MessageElement: FC<MessageElementProps> = (props: MessageElementProps) =>
previewWidth={message.previewWidth} previewHeight={message.previewHeight}
resourcePreviewId={message.resourcePreviewId}
resourceId={message.resourceId}
resourceIdGuild={messageGuild}
resourceName={message.resourceName ?? 'unknown.unk'}
/>
);

View File

@ -3,94 +3,56 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react';
import { Channel, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import React, { FC, useEffect, useMemo } from 'react';
import { Message } from '../../data-types';
import MessageElement from './components/message-element';
import InfiniteScroll from '../components/infinite-scroll';
import { useMessagesScrollingSubscription } from '../require/guild-subscriptions';
import { currGuildActiveChannelMessagesState } from '../require/atoms';
import { currGuildActiveChannelMessagesState, currGuildState } from '../require/atoms';
import { useRecoilValue } from 'recoil';
import { isFailed, isLoaded, isPended, isUnload } from '../require/loadables';
import InfiniteScrollRecoil from '../components/infinite-scroll-recoil';
interface MessageListProps {
guild: CombinedGuild;
channel: Channel;
channelGuild: CombinedGuild;
setFetchRetryCallable: Dispatch<SetStateAction<(() => Promise<void>) | null>>;
}
const MessageList: FC = () => {
// const scrollToBottomFunc = useCallback(() => {
// if (!infiniteScrollElementRef.current) return;
// infiniteScrollElementRef.current.scrollTop = 0; // Keep in mind that this is reversed since we are in flex-flow: column-reverse
// }, []);
const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
const { guild, channel, channelGuild, setFetchRetryCallable } = props;
const infiniteScrollElementRef = useRef<HTMLDivElement>(null);
const scrollToBottomFunc = useCallback(() => {
if (!infiniteScrollElementRef.current) return;
infiniteScrollElementRef.current.scrollTop = 0; // Keep in mind that this is reversed since we are in flex-flow: column-reverse
}, []);
const recoilMessages = useRecoilValue(currGuildActiveChannelMessagesState);
const guild = useRecoilValue(currGuildState);
const messages = useRecoilValue(currGuildActiveChannelMessagesState);
useEffect(() => {
if (isUnload(recoilMessages)) {
if (isUnload(messages)) {
LOG.debug('recoilMessages unloaded');
} else if (isPended(recoilMessages)) {
} else if (isPended(messages)) {
LOG.debug('recoilMessages pended');
} else if (isLoaded(recoilMessages)) {
LOG.debug('recoilMessages loaded. length: ' + recoilMessages.value.length);
} else if (isFailed(recoilMessages)) {
} else if (isLoaded(messages)) {
LOG.debug('recoilMessages loaded. length: ' + messages.value.length);
} else if (isFailed(messages)) {
LOG.debug('recoilMessages failed');
}
// LOG.debug('recoilMessages update', { recoilMessagesLength: recoilMessages.value?.length ?? 'unloaded' });
}, [ recoilMessages ]);
// TODO: Store state higher up
const [
fetchRetryCallable,
fetchAboveCallable,
fetchBelowCallable,
setScrollRatio,
messagesResult,
messagesFetchError,
messagesFetchAboveError,
messagesFetchBelowError
] = useMessagesScrollingSubscription(guild, channel, channelGuild, scrollToBottomFunc);
useEffect(() => {
// Note: have to use a function here otherwise React trys to "help out" by running the promise
setFetchRetryCallable(() => { return fetchRetryCallable });
}, [ fetchRetryCallable ]);
}, [ messages ]);
const messageElements = useMemo(() => {
if (guild === null) return [];
if (!isLoaded(messages)) return [];
const result = [];
if (messagesResult) {
for (let i = 0; i < messagesResult.value.elements.length; ++i) {
const prevMessage = messagesResult.value.elements[i - 1] ?? null;
const message = messagesResult.value.elements[i] as Message;
result.push(<MessageElement key={guild.id + message.id} guild={guild} message={message} prevMessage={prevMessage} messageGuild={messagesResult.guild} />);
}
for (let i = 0; i < messages.value.length; ++i) {
const prevMessage = messages.value[i - 1] ?? null;
const message = messages.value[i] as Message;
result.push(<MessageElement key={guild.id + 'm#' + message.id} guild={guild} message={message} prevMessage={prevMessage} />);
}
return result;
}, [ messagesResult ]);
}, [ messages ]);
return (
<div className="message-list">
<InfiniteScroll
infiniteScrollElementRef={infiniteScrollElementRef}
fetchRetryCallable={fetchRetryCallable}
fetchAboveCallable={fetchAboveCallable}
fetchBelowCallable={fetchBelowCallable}
setScrollRatio={setScrollRatio}
ends={messagesResult?.value.ends ?? null}
fetchError={messagesFetchError}
fetchAboveError={messagesFetchAboveError}
fetchBelowError={messagesFetchBelowError}
fetchErrorMessage="Unable to retrieve recent messages"
fetchAboveErrorMessage="Unable to retrieve messages above"
fetchBelowErrorMessage="Unable to retrieve messages below"
>{messageElements}</InfiniteScroll>
<InfiniteScrollRecoil
scrollable={messages}
initialErrorMessage="Unable to retrieve recent messages"
aboveErrorMessage="Unable to retrieve messages above"
belowErrorMessage="Unable to retrieve messages below"
>{messageElements}</InfiniteScrollRecoil>
</div>
);
}

View File

@ -6,7 +6,7 @@ const LOG = Logger.create(__filename, electronConsole);
import { AtomEffect, Loadable, RecoilValue, useRecoilValueLoadable } from "recoil";
import CombinedGuild from "../../guild-combined";
import { Conflictable, Connectable } from "../../guild-types";
import { createFailedScrollingEnd, createFailedValue, createFailedValueScrolling, createLoadedScrollingEnd, createLoadedValue, createLoadedValueScrolling, createLoadingScrollingEnd, Defined, DEF_PENDED_SCROLLING_VALUE, DEF_PENDED_VALUE, DEF_UNLOADED_SCROLL_END, isEndPended, isLoaded, isPended, LoadableScrollingEnd, LoadableValue, LoadableValueScrolling, LoadedScrollingEnd, LoadedValueScrolling } from "./loadables";
import { createCancelledScrollingEnd, createFailedScrollingEnd, createFailedValue, createFailedValueScrolling, createLoadedScrollingEnd, createLoadedValue, createLoadedValueScrolling, createLoadingScrollingEnd, Defined, DEF_PENDED_SCROLLING_VALUE, DEF_PENDED_VALUE, isEndPended, isLoaded, isPended, LoadableScrollingEnd, LoadableValue, LoadableValueScrolling, LoadedScrollingEnd, LoadedValueScrolling } from "./loadables";
import { guildState } from './atoms';
import { Changes } from '../../data-types';
@ -109,15 +109,16 @@ export function createFetchValueScrollingReferenceFunc<T>(
const selfState = await getPromise(node);
if (!isLoaded(selfState)) return; // Don't send a request if the base LoadableValueScrolling isn't loaded yet
const selfEnd = getFunc(selfState);
let selfEnd = getFunc(selfState);
if (isEndPended(selfEnd)) return; // Don't send a request if we're already loading
canceled = false;
setSelf(applyEndToSelf(selfState, createLoadingScrollingEnd(fetchValueReferenceFunc, cancel)));
selfEnd = createLoadingScrollingEnd(fetchValueReferenceFunc, cancel);
setSelf(applyEndToSelf(selfState, selfEnd));
try {
const result = await fetchReferenceFunc(guild, reference, count);
if (canceled) {
setSelf(applyEndToSelf(selfState, DEF_UNLOADED_SCROLL_END));
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
} else {
const hasMore = result.length >= count;
setSelf(applyResultToSelf(selfState, createLoadedScrollingEnd(hasMore, fetchValueReferenceFunc, cancel), result));
@ -125,7 +126,7 @@ export function createFetchValueScrollingReferenceFunc<T>(
} catch (e: unknown) {
if (canceled) {
LOG.error('unable to fetch value based on reference (but we were canceled)', e);
setSelf(applyEndToSelf(selfState, DEF_UNLOADED_SCROLL_END));
setSelf(applyEndToSelf(selfState, createCancelledScrollingEnd(selfEnd)));
} else {
LOG.error('unable to fetch value based on reference', e);
setSelf(applyEndToSelf(selfState, createFailedScrollingEnd(e, fetchValueReferenceFunc, cancel)));

View File

@ -333,12 +333,12 @@ export function useScrollableCallables<T>(scrollable: LoadableValueScrolling<T[]
const fetchAboveCallable = useCallback(async () => {
if (loadingAbove) return;
setLoadingAbove(true);
if (!isLoaded(scrollable)) return;
if (scrollable.above.hasMore !== true) return; // Don't load unless we know there could be more
if (scrollable.value.length === 0) return; // There's no references available. In this case, hasMore should already have been false/undefined anyway
const topReference = scrollable.value[0] as T;
try {
setLoadingAbove(true);
await scrollable.above.fetch(topReference);
} finally {
if (isMounted.current) {
@ -348,12 +348,12 @@ export function useScrollableCallables<T>(scrollable: LoadableValueScrolling<T[]
}, [ scrollable ]);
const fetchBelowCallable = useCallback(async () => {
if (loadingBelow) return;
setLoadingBelow(true);
if (!isLoaded(scrollable)) return;
if (scrollable.below.hasMore !== true) return; // Don't load unless we know there could be more
if (scrollable.value.length === 0) return; // There's no references available. In this case, hasMore should already have been false/undefined anyway
const bottomReference = scrollable.value[scrollable.value.length - 1] as T;
try {
setLoadingBelow(true);
await scrollable.below.fetch(bottomReference);
} finally {
if (isMounted.current) {
@ -378,7 +378,7 @@ export function useScrollableCallables<T>(scrollable: LoadableValueScrolling<T[]
const distToTop = -(clientHeight - scrollHeight - scrollTop); // keep in mind scrollTop is negative >:]
const distToBottom = -scrollTop;
//LOG.debug(`scroll callable update. to top: ${distToTop}, to bottom: ${distToBottom}`)
LOG.debug(`scroll callable update. to top: ${distToTop}, to bottom: ${distToBottom}`)
if (distToTop < threshold) {
await fetchAboveCallable();

View File

@ -74,7 +74,7 @@ const GuildElement: FC = () => {
<ChannelTitle channel={isLoaded(activeChannel) ? activeChannel.value : null} />
<div className="guild-channel-content">
<div className="guild-channel-feed-wrapper">
{guild && isLoaded(activeChannel) && <MessageList guild={guild} channel={activeChannel.value} channelGuild={guild} setFetchRetryCallable={setFetchMessagesRetryCallable} />}
{guild && isLoaded(activeChannel) && <MessageList />}
{guild && isLoaded(activeChannel) && <SendMessage guild={guild} channel={activeChannel.value} />}
</div>
<div className="member-list-wrapper">