Jump messages to bottom when current user sent the message

This commit is contained in:
Michael Peters 2022-01-23 16:39:14 -06:00
parent ce9b17535b
commit b97db81fa2
6 changed files with 60 additions and 17 deletions

View File

@ -1,8 +1,9 @@
import React, { Dispatch, FC, ReactNode, SetStateAction } from 'react'; import React, { Dispatch, FC, ReactNode, RefObject, SetStateAction } from 'react';
import { useColumnReverseInfiniteScroll } from '../require/react-helper'; import { useColumnReverseInfiniteScroll } from '../require/react-helper';
import Retry from './retry'; import Retry from './retry';
export interface InfiniteScrollProps { export interface InfiniteScrollProps {
infiniteScrollElementRef: RefObject<HTMLDivElement>;
fetchRetryCallable: () => Promise<void>; fetchRetryCallable: () => Promise<void>;
fetchAboveCallable: () => Promise<void>; fetchAboveCallable: () => Promise<void>;
fetchBelowCallable: () => Promise<void>; fetchBelowCallable: () => Promise<void>;
@ -20,6 +21,7 @@ export interface InfiniteScrollProps {
// Implements convenient features such as 'try again' components // Implements convenient features such as 'try again' components
const InfiniteScroll: FC<InfiniteScrollProps> = (props: InfiniteScrollProps) => { const InfiniteScroll: FC<InfiniteScrollProps> = (props: InfiniteScrollProps) => {
const { const {
infiniteScrollElementRef,
fetchRetryCallable, fetchRetryCallable,
fetchAboveCallable, fetchAboveCallable,
fetchBelowCallable, fetchBelowCallable,
@ -47,7 +49,7 @@ const InfiniteScroll: FC<InfiniteScrollProps> = (props: InfiniteScrollProps) =>
); );
return ( return (
<div className="infinite-scroll-scroll-base" onScroll={updateScrollCallable}> <div className="infinite-scroll-scroll-base" ref={infiniteScrollElementRef} onScroll={updateScrollCallable}>
<div className="infinite-scroll-elements"> <div className="infinite-scroll-elements">
<Retry error={fetchAboveError} text={fetchAboveErrorMessage} retryFunc={fetchAboveRetry} /> <Retry error={fetchAboveError} text={fetchAboveErrorMessage} retryFunc={fetchAboveRetry} />
{children} {children}

View File

@ -1,4 +1,4 @@
import React, { Dispatch, FC, ReactNode, SetStateAction, useMemo } from 'react'; import React, { Dispatch, FC, ReactNode, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react';
import { Channel, Message } from '../../data-types'; import { Channel, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
import MessageElement from './components/message-element'; import MessageElement from './components/message-element';
@ -9,12 +9,22 @@ interface MessageListProps {
guild: CombinedGuild; guild: CombinedGuild;
channel: Channel; channel: Channel;
channelGuild: CombinedGuild; channelGuild: CombinedGuild;
setFetchRetryCallable: Dispatch<SetStateAction<(() => Promise<void>) | null>>;
setOverlay: Dispatch<SetStateAction<ReactNode>>; setOverlay: Dispatch<SetStateAction<ReactNode>>;
} }
const MessageList: FC<MessageListProps> = (props: MessageListProps) => { const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
const { guild, channel, channelGuild, setOverlay } = props; const { guild, channel, channelGuild, setFetchRetryCallable, setOverlay } = props;
const infiniteScrollElementRef = useRef<HTMLDivElement>(null);
const scrollToBottomFunc = useCallback(() => {
if (!infiniteScrollElementRef.current) return;
console.log('scrolling to bottom');
infiniteScrollElementRef.current.scrollTop = 0; // Keep in mind that this is reversed for flex-flow: column-reverse
}, []);
// TODO: Store state higher up
const [ const [
fetchRetryCallable, fetchRetryCallable,
fetchAboveCallable, fetchAboveCallable,
@ -24,7 +34,12 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
messagesFetchError, messagesFetchError,
messagesFetchAboveError, messagesFetchAboveError,
messagesFetchBelowError messagesFetchBelowError
] = useMessagesScrollingSubscription(guild, channel, channelGuild); ] = 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 ]);
const messageElements = useMemo(() => { const messageElements = useMemo(() => {
const result = []; const result = [];
@ -41,6 +56,7 @@ const MessageList: FC<MessageListProps> = (props: MessageListProps) => {
return ( return (
<div className="message-list"> <div className="message-list">
<InfiniteScroll <InfiniteScroll
infiniteScrollElementRef={infiniteScrollElementRef}
fetchRetryCallable={fetchRetryCallable} fetchRetryCallable={fetchRetryCallable}
fetchAboveCallable={fetchAboveCallable} fetchAboveCallable={fetchAboveCallable}
fetchBelowCallable={fetchBelowCallable} fetchBelowCallable={fetchBelowCallable}

View File

@ -350,6 +350,7 @@ function useMultipleGuildSubscriptionScrolling<
fetchFunc: () => Promise<T[] | null>, fetchFunc: () => Promise<T[] | null>,
fetchAboveFunc: ((reference: T) => Promise<T[] | null>), fetchAboveFunc: ((reference: T) => Promise<T[] | null>),
fetchBelowFunc: ((reference: T) => Promise<T[] | null>), fetchBelowFunc: ((reference: T) => Promise<T[] | null>),
scrollToBottomFunc: () => void,
): [ ): [
fetchRetryCallable: () => Promise<void>, fetchRetryCallable: () => Promise<void>,
fetchAboveCallable: () => Promise<void>, fetchAboveCallable: () => Promise<void>,
@ -369,6 +370,8 @@ function useMultipleGuildSubscriptionScrolling<
} = eventMappingParams; } = eventMappingParams;
const isMounted = useIsMountedRef(); const isMounted = useIsMountedRef();
const guildRef = useRef<CombinedGuild>(guild);
guildRef.current = guild;
// TODO: lastResult.value should really be only T[] instead of | null since we set it to [] anyway in the onUpdate, etc functions // TODO: lastResult.value should really be only T[] instead of | null since we set it to [] anyway in the onUpdate, etc functions
const [ lastResult, setLastResult ] = useState<ScrollingSubscriptionResult<T> | null>(null); const [ lastResult, setLastResult ] = useState<ScrollingSubscriptionResult<T> | null>(null);
@ -535,6 +538,7 @@ function useMultipleGuildSubscriptionScrolling<
guild: fetchValueGuild guild: fetchValueGuild
}); });
setFetchError(null); setFetchError(null);
scrollToBottomFunc(); // Make sure that we scroll back to the bottom
}, [ sortFunc, maxFetchElements, maxElements ]); }, [ sortFunc, maxFetchElements, maxElements ]);
const onFetchError = useCallback((e: unknown) => { const onFetchError = useCallback((e: unknown) => {
@ -799,7 +803,7 @@ export function useTokensSubscription(guild: CombinedGuild) {
}, fetchTokensFunc); }, fetchTokensFunc);
} }
export function useMessagesScrollingSubscription(guild: CombinedGuild, channel: Channel, channelGuild: CombinedGuild) { export function useMessagesScrollingSubscription(guild: CombinedGuild, channel: Channel, channelGuild: CombinedGuild, scrollToBottomFunc: () => void) {
const maxFetchElements = Globals.MESSAGES_PER_REQUEST; const maxFetchElements = Globals.MESSAGES_PER_REQUEST;
const maxElements = Globals.MAX_CURRENT_MESSAGES; const maxElements = Globals.MAX_CURRENT_MESSAGES;
const fetchMessagesFunc = useCallback(async () => { const fetchMessagesFunc = useCallback(async () => {
@ -829,7 +833,8 @@ export function useMessagesScrollingSubscription(guild: CombinedGuild, channel:
sortFunc: Message.sortOrder sortFunc: Message.sortOrder
}, },
maxElements, maxFetchElements, maxElements, maxFetchElements,
fetchMessagesFunc, fetchAboveFunc, fetchBelowFunc fetchMessagesFunc, fetchAboveFunc, fetchBelowFunc,
) scrollToBottomFunc
);
} }

View File

@ -3,8 +3,8 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger'; import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole); const LOG = Logger.create(__filename, electronConsole);
import React, { Dispatch, FC, ReactNode, SetStateAction, useEffect, useState } from 'react'; import React, { Dispatch, FC, ReactNode, SetStateAction, useCallback, useEffect, useState } from 'react';
import { Channel } from '../../data-types'; import { Channel, Message } from '../../data-types';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
import ChannelList from '../lists/channel-list'; import ChannelList from '../lists/channel-list';
import MemberList from '../lists/member-list'; import MemberList from '../lists/member-list';
@ -36,6 +36,8 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
const [ activeChannel, setActiveChannel ] = useState<Channel | null>(null); const [ activeChannel, setActiveChannel ] = useState<Channel | null>(null);
const [ activeChannelGuild, setActiveChannelGuild ] = useState<CombinedGuild | null>(null); const [ activeChannelGuild, setActiveChannelGuild ] = useState<CombinedGuild | null>(null);
const [ fetchMessagesRetryCallable, setFetchMessagesRetryCallable ] = useState<(() => Promise<void>) | null>(null);
useEffect(() => { useEffect(() => {
setActiveChannel(null); setActiveChannel(null);
}, [ guild ]); }, [ guild ]);
@ -57,6 +59,28 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
} }
}, [ channelsResult, activeChannel ]); }, [ channelsResult, activeChannel ]);
// When the self member sends a message to the active channel and we get it as a new message,
// move the active channel's messages back to the bottom
const onNewMessage = useCallback((messages: Message[]) => {
if (messages.find(message =>
message.member.id === selfMemberResult?.value?.id &&
message.channel.id === activeChannel?.id)
) {
if (fetchMessagesRetryCallable) {
// TODO: Only do the fetch if we're not at the bottom. If we're at the bottom, just
// set the scroll height
fetchMessagesRetryCallable(); // Re-load the messages to move them to the bottom.
}
}
}, [ fetchMessagesRetryCallable/*, Note: Removing these to prevent re-sending fetch. This technique should probably be changed in the future with react/redux selfMemberResult, activeChannel */ ]);
useEffect(() => {
guild.on('new-messages', onNewMessage);
return () => {
guild.off('new-messages', onNewMessage);
}
});
return ( return (
<div className="guild-react"> <div className="guild-react">
<div className="guild-sidebar"> <div className="guild-sidebar">
@ -73,7 +97,7 @@ const GuildElement: FC<GuildElementProps> = (props: GuildElementProps) => {
<ChannelTitle channel={activeChannel} /> <ChannelTitle channel={activeChannel} />
<div className="guild-channel-content"> <div className="guild-channel-content">
<div className="guild-channel-feed-wrapper"> <div className="guild-channel-feed-wrapper">
{activeChannel && activeChannelGuild && <MessageList guild={guild} channel={activeChannel} channelGuild={activeChannelGuild} setOverlay={setOverlay} />} {activeChannel && activeChannelGuild && <MessageList guild={guild} channel={activeChannel} channelGuild={activeChannelGuild} setFetchRetryCallable={setFetchMessagesRetryCallable} setOverlay={setOverlay} />}
{activeChannel && activeChannelGuild && <SendMessage guild={guild} channel={activeChannel} />} {activeChannel && activeChannelGuild && <SendMessage guild={guild} channel={activeChannel} />}
</div> </div>
<div className="member-list-wrapper"> <div className="member-list-wrapper">

View File

@ -1,8 +1,3 @@
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, { ClipboardEvent, FC, FormEvent, KeyboardEvent, RefObject, useCallback, useMemo, useRef, useState } from 'react'; import React, { ClipboardEvent, FC, FormEvent, KeyboardEvent, RefObject, useCallback, useMemo, useRef, useState } from 'react';
import { Channel } from '../../data-types'; import { Channel } from '../../data-types';
import CombinedGuild from '../../guild-combined'; import CombinedGuild from '../../guild-combined';
@ -79,6 +74,7 @@ const SendMessage: FC<SendMessageProps> = (props: SendMessageProps) => {
if (!isMounted.current) return; if (!isMounted.current) return;
} }
setText(''); setText('');
if (contentEditableRef.current) contentEditableRef.current.innerText = ''; if (contentEditableRef.current) contentEditableRef.current.innerText = '';
setEnabled(true); setEnabled(true);

View File

@ -56,7 +56,7 @@ window.addEventListener('DOMContentLoaded', () => {
const guildsManager = new GuildsManager(messageRAMCache, resourceRAMCache, personalDB); const guildsManager = new GuildsManager(messageRAMCache, resourceRAMCache, personalDB);
await guildsManager.init(); await guildsManager.init();
LOG.silly('events bound'); LOG.silly('guildsManager initialized');
ReactDOM.render(<RootElement guildsManager={guildsManager} />, document.getElementById('react-root')); ReactDOM.render(<RootElement guildsManager={guildsManager} />, document.getElementById('react-root'));
})(); })();