add infinite scroll "jump to bottom" ui components

This commit is contained in:
Michael Peters 2022-12-12 20:53:20 -08:00
parent f60831454f
commit 29b7c47a7e
7 changed files with 77 additions and 20 deletions

View File

@ -0,0 +1,31 @@
@use '../../styles/theme.scss';
.jump-to-bottom-wrapper {
position: relative;
.jump-to-bottom {
box-sizing: border-box;
position: absolute;
font-size: 0.85em;
left: 0;
bottom: 52px;
padding: 4px 8px 16px 8px;
margin-left: 16px;
width: calc(100% - 32px);
display: flex;
justify-content: space-between;
border-radius: 8px;
color: theme.$header-primary;
background-color: theme.$background-input-alt;
.jump {
cursor: pointer;
font-weight: 600;
}
}
}

View File

@ -1,4 +1,4 @@
import React, { MutableRefObject, ReactNode } from 'react'; import React, { useMemo, MutableRefObject, ReactNode } from 'react';
import { LoadableValueScrolling } from '../require/loadables'; import { LoadableValueScrolling } from '../require/loadables';
import { useScrollableCallables } from '../require/react-helper'; import { useScrollableCallables } from '../require/react-helper';
@ -18,24 +18,44 @@ function InfiniteScrollRecoil<T>(props: InfiniteScrollRecoilProps<T[], T>) {
const { fetchAboveCallable, fetchBelowCallable, onScrollCallable } = useScrollableCallables(scrollable, 600); // activate fetch above/below when 600 client px from the top/bottom const { fetchAboveCallable, fetchBelowCallable, onScrollCallable } = useScrollableCallables(scrollable, 600); // activate fetch above/below when 600 client px from the top/bottom
const jumpToBottom = useMemo(() => {
if (!scrollable.below?.hasMore) {
return null;
}
return (
<div className="jump-to-bottom-wrapper">
<div className="jump-to-bottom">
<div className="text">You are viewing older messages</div>
<div className="jump" onClick={async () => { console.log('jump to bottom'); }}>
Jump to Bottom
</div>
</div>
</div>
);
}, [scrollable]);
return ( return (
<div ref={infiniteScrollRef} className="infinite-scroll-scroll-base" onScroll={onScrollCallable}> <div>
<div className="infinite-scroll-elements"> <div ref={infiniteScrollRef} className="infinite-scroll-scroll-base" onScroll={onScrollCallable}>
<Retry error={scrollable?.above?.error} text={aboveErrorMessage} retryFunc={fetchAboveCallable} /> <div className="infinite-scroll-elements">
{children} <Retry error={scrollable.above?.error} text={aboveErrorMessage} retryFunc={fetchAboveCallable} />
<Retry error={scrollable?.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} /> {children}
<Retry <Retry error={scrollable.below?.error} text={belowErrorMessage} retryFunc={fetchBelowCallable} />
error={scrollable.error} <Retry
text={initialErrorMessage} error={scrollable.error}
retryFunc={ text={initialErrorMessage}
scrollable.retry ?? // TODO: Allow null instead of noop func to prevent re-renders
(async () => { retryFunc={
/* do nothing if we are unloaded */ scrollable.retry ??
}) (async () => {
} /* do nothing if we are unloaded */
/> })
</div> }
</div> />
</div>
</div>
{jumpToBottom}
</div>
); );
} }

View File

@ -343,7 +343,7 @@ export function useScrollableCallables<T>(
const distToTop = -(clientHeight - scrollHeight - scrollTop); // keep in mind scrollTop is negative >:] const distToTop = -(clientHeight - scrollHeight - scrollTop); // keep in mind scrollTop is negative >:]
const distToBottom = -scrollTop; 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 && isLoaded(scrollable) && !isEndFailed(scrollable.above)) { if (distToTop < threshold && isLoaded(scrollable) && !isEndFailed(scrollable.above)) {
await fetchAboveCallable(); await fetchAboveCallable();

View File

@ -392,7 +392,7 @@ export default class CombinedGuild
return messages; return messages;
} }
async fetchMessagesAfter(channelId: string, messageOrderId: string, number: number): Promise<Message[]> { async fetchMessagesAfter(channelId: string, messageOrderId: string, number: number): Promise<Message[]> {
Util.failSometimes(0.75); // for testing // xUtil.failSometimes(0.05); // for testing
LOG.debug(`g#${this.id}: fetch messages after ch#${channelId.slice(0, 4)}, mo#${messageOrderId}, ${number}`); LOG.debug(`g#${this.id}: fetch messages after ch#${channelId.slice(0, 4)}, mo#${messageOrderId}, ${number}`);
const members = await this.grabRAMMembersMap(); const members = await this.grabRAMMembersMap();
const channels = await this.grabRAMChannelsMap(); const channels = await this.grabRAMChannelsMap();

View File

@ -9,6 +9,7 @@
@use 'elements-styles/components/buttons.scss'; @use 'elements-styles/components/buttons.scss';
@use 'elements-styles/components/file-drop-target.scss'; @use 'elements-styles/components/file-drop-target.scss';
@use 'elements-styles/components/infinite-scroll.scss';
@use 'elements-styles/components/input-dropdown.scss'; @use 'elements-styles/components/input-dropdown.scss';
@use 'elements-styles/components/input-image-edit.scss'; @use 'elements-styles/components/input-image-edit.scss';
@use 'elements-styles/components/input-text.scss'; @use 'elements-styles/components/input-text.scss';

View File

@ -29,6 +29,7 @@ $background-popup-message: rgba(30, 31, 34, 0.75);
$background-primary-divider: #3f4149; $background-primary-divider: #3f4149;
$background-input: #2f3136; $background-input: #2f3136;
$background-input-alt: #646872;
$border-input: #1d1e22; $border-input: #1d1e22;
$border-input-hover: #0b0c0e; $border-input-hover: #0b0c0e;
$border-input-focus: #0099ff; $border-input-focus: #0099ff;

View File

@ -16,8 +16,12 @@ process.on('unhandledRejection', async (reason, _promise) => {
const targetGuild = guilds.find(guild => guild.name === 'no chicoms'); const targetGuild = guilds.find(guild => guild.name === 'no chicoms');
const members = await DB.getMembers(targetGuild.id); const members = await DB.getMembers(targetGuild.id);
const targetMember = members.find(member => member.display_name === 'Elipzer'); const targetMember = members.find(member => member.display_name === 'Elipzer');
if (targetMember === undefined) {
LOG.error('unable to find target member');
}
const channels = await DB.getChannels(targetGuild.id); const channels = await DB.getChannels(targetGuild.id);
const targetChannel = channels.find(channel => channel.name === 'memes'); const targetChannel = channels.find(channel => channel.name === 'memes');
LOG.debug('inserting testing messages...');
for (let i = 0; i < 2000; ++i) { for (let i = 0; i < 2000; ++i) {
await DB.insertMessage(targetGuild.id, targetChannel.id, targetMember.id, 'Test Message #' + i); await DB.insertMessage(targetGuild.id, targetChannel.id, targetMember.id, 'Test Message #' + i);
} }