mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): scope near-viewport loading to the surrounding scroller (#1863)
This commit is contained in:
1 parent
943b948de7
commit
15f4417c68
4 files changed
+265
-222
No files matched your search
@@ -9,6 +9,7 @@ import {JUMP_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors
|
||||
import styles from '@app/features/messaging/components/pages/MessageListPage.module.css';
|
||||
import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
|
||||
import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy';
|
||||
import {NearViewportSurfaceContext} from '@app/features/messaging/hooks/useNearViewport';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils';
|
||||
import {goToMessage} from '@app/features/messaging/utils/MessageNavigator';
|
||||
@@ -20,7 +21,7 @@ import {useLingui} from '@lingui/react/macro';
|
||||
import {FlagCheckeredIcon, SparkleIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useRef} from 'react';
|
||||
import {useMemo, useRef} from 'react';
|
||||
|
||||
const YOU_VE_REACHED_THE_END_DESCRIPTOR = msg({
|
||||
message: "You've reached the end",
|
||||
@@ -51,6 +52,7 @@ export const MessageListPage = observer(
|
||||
}: MessageListPageProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||
const resolveListPageScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||
const onCopySelectedMessages = useMessageSelectionCopyForMessages<HTMLDivElement>(messages);
|
||||
const leftContent = (
|
||||
<div className={styles.header} data-flx="messaging.message-list-page.header">
|
||||
@@ -73,77 +75,79 @@ export const MessageListPage = observer(
|
||||
/>
|
||||
<div className={styles.content} data-flx="messaging.message-list-page.content">
|
||||
{messages.length > 0 ? (
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
key="message-list-page-scroller"
|
||||
ref={scrollerRef}
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="messaging.message-list-page.scroller"
|
||||
>
|
||||
{messages.map((message) => {
|
||||
const channel = Channels.getChannel(message.channelId);
|
||||
if (!channel) {
|
||||
if (renderMissingMessage) {
|
||||
return renderMissingMessage(message);
|
||||
<NearViewportSurfaceContext.Provider value={resolveListPageScrollSurface}>
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
key="message-list-page-scroller"
|
||||
ref={scrollerRef}
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="messaging.message-list-page.scroller"
|
||||
>
|
||||
{messages.map((message) => {
|
||||
const channel = Channels.getChannel(message.channelId);
|
||||
if (!channel) {
|
||||
if (renderMissingMessage) {
|
||||
return renderMissingMessage(message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={previewStyles.previewCard}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start="true"
|
||||
data-flx="messaging.message-list-page.div"
|
||||
>
|
||||
<MessageComponent
|
||||
message={message}
|
||||
channel={channel}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
data-flx="messaging.message-list-page.message-component"
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={previewStyles.previewCard}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start="true"
|
||||
data-flx="messaging.message-list-page.div"
|
||||
>
|
||||
<MessageComponent
|
||||
message={message}
|
||||
channel={channel}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
data-flx="messaging.message-list-page.message-component"
|
||||
/>
|
||||
<div className={previewStyles.actionButtons} data-flx="messaging.message-list-page.div--2">
|
||||
<button
|
||||
type="button"
|
||||
className={previewStyles.actionButton}
|
||||
onClick={() => {
|
||||
const path = channel.guildId
|
||||
? Routes.guildChannel(channel.guildId, channel.id)
|
||||
: Routes.dmChannel(channel.id);
|
||||
RouterUtils.transitionTo(path);
|
||||
goToMessage(message.channelId, message.id);
|
||||
focusChannelTextareaAfterNavigation(message.channelId);
|
||||
}}
|
||||
data-flx="messaging.message-list-page.button.transition-to"
|
||||
>
|
||||
{i18n._(JUMP_DESCRIPTOR)}
|
||||
</button>
|
||||
{renderActionButtons(message)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.endState} data-flx="messaging.message-list-page.end-state">
|
||||
<div className={styles.endStateContent} data-flx="messaging.message-list-page.end-state-content">
|
||||
<FlagCheckeredIcon
|
||||
className={styles.endStateIcon}
|
||||
data-flx="messaging.message-list-page.end-state-icon"
|
||||
/>
|
||||
<div className={previewStyles.actionButtons} data-flx="messaging.message-list-page.div--2">
|
||||
<button
|
||||
type="button"
|
||||
className={previewStyles.actionButton}
|
||||
onClick={() => {
|
||||
const path = channel.guildId
|
||||
? Routes.guildChannel(channel.guildId, channel.id)
|
||||
: Routes.dmChannel(channel.id);
|
||||
RouterUtils.transitionTo(path);
|
||||
goToMessage(message.channelId, message.id);
|
||||
focusChannelTextareaAfterNavigation(message.channelId);
|
||||
}}
|
||||
data-flx="messaging.message-list-page.button.transition-to"
|
||||
<div className={styles.endStateText} data-flx="messaging.message-list-page.end-state-text">
|
||||
<h3 className={styles.endStateTitle} data-flx="messaging.message-list-page.end-state-title">
|
||||
{i18n._(YOU_VE_REACHED_THE_END_DESCRIPTOR)}
|
||||
</h3>
|
||||
<p
|
||||
className={styles.endStateDescription}
|
||||
data-flx="messaging.message-list-page.end-state-description"
|
||||
>
|
||||
{i18n._(JUMP_DESCRIPTOR)}
|
||||
</button>
|
||||
{renderActionButtons(message)}
|
||||
{endStateDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.endState} data-flx="messaging.message-list-page.end-state">
|
||||
<div className={styles.endStateContent} data-flx="messaging.message-list-page.end-state-content">
|
||||
<FlagCheckeredIcon
|
||||
className={styles.endStateIcon}
|
||||
data-flx="messaging.message-list-page.end-state-icon"
|
||||
/>
|
||||
<div className={styles.endStateText} data-flx="messaging.message-list-page.end-state-text">
|
||||
<h3 className={styles.endStateTitle} data-flx="messaging.message-list-page.end-state-title">
|
||||
{i18n._(YOU_VE_REACHED_THE_END_DESCRIPTOR)}
|
||||
</h3>
|
||||
<p
|
||||
className={styles.endStateDescription}
|
||||
data-flx="messaging.message-list-page.end-state-description"
|
||||
>
|
||||
{endStateDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
</Scroller>
|
||||
</NearViewportSurfaceContext.Provider>
|
||||
) : (
|
||||
<div className={styles.emptyState} data-flx="messaging.message-list-page.empty-state">
|
||||
<div className={styles.emptyStateContent} data-flx="messaging.message-list-page.empty-state-content">
|
||||
|
||||
@@ -18,6 +18,7 @@ import headerStyles from '@app/features/messaging/components/popouts/InboxMessag
|
||||
import styles from '@app/features/messaging/components/popouts/RecentMentionsContent.module.css';
|
||||
import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
|
||||
import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy';
|
||||
import {NearViewportSurfaceContext} from '@app/features/messaging/hooks/useNearViewport';
|
||||
import type {Message as MessagingMessage} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {goToMessage} from '@app/features/messaging/utils/MessageNavigator';
|
||||
import * as RecentMentionCommands from '@app/features/notification/commands/RecentMentionCommands';
|
||||
@@ -217,6 +218,7 @@ export const RecentMentionsContent = observer(
|
||||
const isLoadingMore = MentionFeed.getIsLoadingMore();
|
||||
const filterButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||
const resolveMentionsScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||
const {isOpen: isFilterMenuOpen, withTracking} = useContextMenuTrigger();
|
||||
const accessibleMentions = MentionFeed.getAccessibleMentions();
|
||||
const onCopySelectedMessages = useMessageSelectionCopyForMessages<HTMLDivElement>(accessibleMentions);
|
||||
@@ -354,64 +356,69 @@ export const RecentMentionsContent = observer(
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
onScroll={handleScroll}
|
||||
key="recent-mentions-scroller"
|
||||
ref={scrollerRef}
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="messaging.recent-mentions-content.scroller"
|
||||
>
|
||||
{groupedMentions.map((group) => {
|
||||
return (
|
||||
<section
|
||||
key={group.key}
|
||||
className={styles.guildGroup}
|
||||
aria-label={group.label}
|
||||
data-flx="messaging.recent-mentions-content.guild-group"
|
||||
>
|
||||
{group.mentions.map((message) => {
|
||||
const channel = Channels.getChannel(message.channelId);
|
||||
if (!channel) return null;
|
||||
return (
|
||||
<MentionMessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
channel={channel}
|
||||
onJump={handleJumpToMessage}
|
||||
onRemove={handleRemoveMention}
|
||||
data-flx="messaging.recent-mentions-content.mention-message-card"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{isLoadingMore && (
|
||||
<div className={previewStyles.loadingState} data-flx="messaging.recent-mentions-content.div--7">
|
||||
<Spinner data-flx="messaging.recent-mentions-content.spinner--2" />
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && !isLoadingMore && (
|
||||
<div className={previewStyles.endState} data-flx="messaging.recent-mentions-content.div--8">
|
||||
<div className={previewStyles.endStateContent} data-flx="messaging.recent-mentions-content.div--9">
|
||||
<FlagCheckeredIcon
|
||||
className={previewStyles.endStateIcon}
|
||||
data-flx="messaging.recent-mentions-content.flag-checkered-icon"
|
||||
/>
|
||||
<div className={previewStyles.endStateTextContainer} data-flx="messaging.recent-mentions-content.div--10">
|
||||
<h3 className={previewStyles.endStateTitle} data-flx="messaging.recent-mentions-content.h3--2">
|
||||
<Trans>You've reached the end</Trans>
|
||||
</h3>
|
||||
<p className={previewStyles.endStateDescription} data-flx="messaging.recent-mentions-content.p--2">
|
||||
<Trans>You've seen all your recent mentions. More will appear here soon.</Trans>
|
||||
</p>
|
||||
<NearViewportSurfaceContext.Provider value={resolveMentionsScrollSurface}>
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
onScroll={handleScroll}
|
||||
key="recent-mentions-scroller"
|
||||
ref={scrollerRef}
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="messaging.recent-mentions-content.scroller"
|
||||
>
|
||||
{groupedMentions.map((group) => {
|
||||
return (
|
||||
<section
|
||||
key={group.key}
|
||||
className={styles.guildGroup}
|
||||
aria-label={group.label}
|
||||
data-flx="messaging.recent-mentions-content.guild-group"
|
||||
>
|
||||
{group.mentions.map((message) => {
|
||||
const channel = Channels.getChannel(message.channelId);
|
||||
if (!channel) return null;
|
||||
return (
|
||||
<MentionMessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
channel={channel}
|
||||
onJump={handleJumpToMessage}
|
||||
onRemove={handleRemoveMention}
|
||||
data-flx="messaging.recent-mentions-content.mention-message-card"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{isLoadingMore && (
|
||||
<div className={previewStyles.loadingState} data-flx="messaging.recent-mentions-content.div--7">
|
||||
<Spinner data-flx="messaging.recent-mentions-content.spinner--2" />
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && !isLoadingMore && (
|
||||
<div className={previewStyles.endState} data-flx="messaging.recent-mentions-content.div--8">
|
||||
<div className={previewStyles.endStateContent} data-flx="messaging.recent-mentions-content.div--9">
|
||||
<FlagCheckeredIcon
|
||||
className={previewStyles.endStateIcon}
|
||||
data-flx="messaging.recent-mentions-content.flag-checkered-icon"
|
||||
/>
|
||||
<div
|
||||
className={previewStyles.endStateTextContainer}
|
||||
data-flx="messaging.recent-mentions-content.div--10"
|
||||
>
|
||||
<h3 className={previewStyles.endStateTitle} data-flx="messaging.recent-mentions-content.h3--2">
|
||||
<Trans>You've reached the end</Trans>
|
||||
</h3>
|
||||
<p className={previewStyles.endStateDescription} data-flx="messaging.recent-mentions-content.p--2">
|
||||
<Trans>You've seen all your recent mentions. More will appear here soon.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Scroller>
|
||||
)}
|
||||
</Scroller>
|
||||
</NearViewportSurfaceContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -13,6 +13,7 @@ import headerStyles from '@app/features/messaging/components/popouts/InboxMessag
|
||||
import styles from '@app/features/messaging/components/popouts/SavedMessagesContent.module.css';
|
||||
import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
|
||||
import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy';
|
||||
import {NearViewportSurfaceContext} from '@app/features/messaging/hooks/useNearViewport';
|
||||
import SavedMessages from '@app/features/messaging/state/SavedMessages';
|
||||
import {goToMessage} from '@app/features/messaging/utils/MessageNavigator';
|
||||
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||
@@ -24,7 +25,7 @@ import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {FlagCheckeredIcon, SparkleIcon, XIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useCallback, useEffect, useMemo, useRef} from 'react';
|
||||
|
||||
const NO_BOOKMARKS_DESCRIPTOR = msg({
|
||||
message: 'No bookmarks',
|
||||
@@ -49,6 +50,7 @@ export const SavedMessagesContent = observer(() => {
|
||||
const {i18n} = useLingui();
|
||||
const {savedMessages, missingSavedMessages, fetched} = SavedMessages;
|
||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||
const resolveSavedScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||
const onCopySelectedMessages = useMessageSelectionCopyForMessages<HTMLDivElement>(savedMessages);
|
||||
const renderMissingSavedMessage = useCallback(
|
||||
(entryId: string) => (
|
||||
@@ -105,99 +107,105 @@ export const SavedMessagesContent = observer(() => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
key="saved-messages-scroller"
|
||||
ref={scrollerRef}
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="messaging.saved-messages-content.scroller"
|
||||
>
|
||||
{missingSavedMessages.map((entry) => renderMissingSavedMessage(entry.id))}
|
||||
{savedMessages.map((message) => {
|
||||
const channel = Channels.getChannel(message.channelId);
|
||||
if (!channel) {
|
||||
return renderMissingSavedMessage(message.id);
|
||||
}
|
||||
return (
|
||||
<div key={message.id} className={styles.messageCard} data-flx="messaging.saved-messages-content.message-card">
|
||||
<InboxMessageHeader
|
||||
channel={channel}
|
||||
onClick={() => handleJumpToMessage(message.channelId, message.id)}
|
||||
rightActions={
|
||||
<Tooltip
|
||||
text={i18n._(REMOVE_BOOKMARK_DESCRIPTOR)}
|
||||
position="top"
|
||||
data-flx="messaging.saved-messages-content.tooltip"
|
||||
>
|
||||
<FocusRing offset={-2} data-flx="messaging.saved-messages-content.focus-ring">
|
||||
<NearViewportSurfaceContext.Provider value={resolveSavedScrollSurface}>
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
key="saved-messages-scroller"
|
||||
ref={scrollerRef}
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="messaging.saved-messages-content.scroller"
|
||||
>
|
||||
{missingSavedMessages.map((entry) => renderMissingSavedMessage(entry.id))}
|
||||
{savedMessages.map((message) => {
|
||||
const channel = Channels.getChannel(message.channelId);
|
||||
if (!channel) {
|
||||
return renderMissingSavedMessage(message.id);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={styles.messageCard}
|
||||
data-flx="messaging.saved-messages-content.message-card"
|
||||
>
|
||||
<InboxMessageHeader
|
||||
channel={channel}
|
||||
onClick={() => handleJumpToMessage(message.channelId, message.id)}
|
||||
rightActions={
|
||||
<Tooltip
|
||||
text={i18n._(REMOVE_BOOKMARK_DESCRIPTOR)}
|
||||
position="top"
|
||||
data-flx="messaging.saved-messages-content.tooltip"
|
||||
>
|
||||
<FocusRing offset={-2} data-flx="messaging.saved-messages-content.focus-ring">
|
||||
<button
|
||||
type="button"
|
||||
className={headerStyles.headerIconButton}
|
||||
onClick={() => SavedMessageCommands.remove(i18n, message.id)}
|
||||
aria-label={i18n._(REMOVE_BOOKMARK_DESCRIPTOR)}
|
||||
data-flx="messaging.saved-messages-content.button.remove"
|
||||
>
|
||||
<XIcon
|
||||
weight="bold"
|
||||
className={headerStyles.headerIcon}
|
||||
data-flx="messaging.saved-messages-content.x-icon"
|
||||
/>
|
||||
</button>
|
||||
</FocusRing>
|
||||
</Tooltip>
|
||||
}
|
||||
data-flx="messaging.saved-messages-content.inbox-message-header.jump-to-message"
|
||||
/>
|
||||
<div
|
||||
className={previewStyles.previewCard}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start="true"
|
||||
data-flx="messaging.saved-messages-content.div--5"
|
||||
>
|
||||
<Message
|
||||
message={message}
|
||||
channel={channel}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
behaviorOverrides={readonlyBehaviorOverrides}
|
||||
suppressMessageActions
|
||||
onHeadingActivate={() => handleJumpToMessage(message.channelId, message.id)}
|
||||
data-flx="messaging.saved-messages-content.message"
|
||||
/>
|
||||
<div className={previewStyles.actionButtons} data-flx="messaging.saved-messages-content.div--6">
|
||||
<FocusRing offset={-2} data-flx="messaging.saved-messages-content.focus-ring--2">
|
||||
<button
|
||||
type="button"
|
||||
className={headerStyles.headerIconButton}
|
||||
onClick={() => SavedMessageCommands.remove(i18n, message.id)}
|
||||
aria-label={i18n._(REMOVE_BOOKMARK_DESCRIPTOR)}
|
||||
data-flx="messaging.saved-messages-content.button.remove"
|
||||
className={previewStyles.actionButton}
|
||||
onClick={() => {
|
||||
handleJumpToMessage(message.channelId, message.id);
|
||||
}}
|
||||
data-flx="messaging.saved-messages-content.button.jump-to-message"
|
||||
>
|
||||
<XIcon
|
||||
weight="bold"
|
||||
className={headerStyles.headerIcon}
|
||||
data-flx="messaging.saved-messages-content.x-icon"
|
||||
/>
|
||||
{i18n._(JUMP_DESCRIPTOR)}
|
||||
</button>
|
||||
</FocusRing>
|
||||
</Tooltip>
|
||||
}
|
||||
data-flx="messaging.saved-messages-content.inbox-message-header.jump-to-message"
|
||||
/>
|
||||
<div
|
||||
className={previewStyles.previewCard}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start="true"
|
||||
data-flx="messaging.saved-messages-content.div--5"
|
||||
>
|
||||
<Message
|
||||
message={message}
|
||||
channel={channel}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
behaviorOverrides={readonlyBehaviorOverrides}
|
||||
suppressMessageActions
|
||||
onHeadingActivate={() => handleJumpToMessage(message.channelId, message.id)}
|
||||
data-flx="messaging.saved-messages-content.message"
|
||||
/>
|
||||
<div className={previewStyles.actionButtons} data-flx="messaging.saved-messages-content.div--6">
|
||||
<FocusRing offset={-2} data-flx="messaging.saved-messages-content.focus-ring--2">
|
||||
<button
|
||||
type="button"
|
||||
className={previewStyles.actionButton}
|
||||
onClick={() => {
|
||||
handleJumpToMessage(message.channelId, message.id);
|
||||
}}
|
||||
data-flx="messaging.saved-messages-content.button.jump-to-message"
|
||||
>
|
||||
{i18n._(JUMP_DESCRIPTOR)}
|
||||
</button>
|
||||
</FocusRing>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={previewStyles.endState} data-flx="messaging.saved-messages-content.div--7">
|
||||
<div className={previewStyles.endStateContent} data-flx="messaging.saved-messages-content.div--8">
|
||||
<FlagCheckeredIcon
|
||||
className={previewStyles.endStateIcon}
|
||||
data-flx="messaging.saved-messages-content.flag-checkered-icon"
|
||||
/>
|
||||
<div className={previewStyles.endStateTextContainer} data-flx="messaging.saved-messages-content.div--9">
|
||||
<h3 className={previewStyles.endStateTitle} data-flx="messaging.saved-messages-content.h3--2">
|
||||
{i18n._(YOU_VE_REACHED_THE_END_DESCRIPTOR)}
|
||||
</h3>
|
||||
<p className={previewStyles.endStateDescription} data-flx="messaging.saved-messages-content.p--2">
|
||||
{i18n._(THERE_S_NOTHING_MORE_TO_SEE_HERE_DESCRIPTOR)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
<div className={previewStyles.endState} data-flx="messaging.saved-messages-content.div--7">
|
||||
<div className={previewStyles.endStateContent} data-flx="messaging.saved-messages-content.div--8">
|
||||
<FlagCheckeredIcon
|
||||
className={previewStyles.endStateIcon}
|
||||
data-flx="messaging.saved-messages-content.flag-checkered-icon"
|
||||
/>
|
||||
<div className={previewStyles.endStateTextContainer} data-flx="messaging.saved-messages-content.div--9">
|
||||
<h3 className={previewStyles.endStateTitle} data-flx="messaging.saved-messages-content.h3--2">
|
||||
{i18n._(YOU_VE_REACHED_THE_END_DESCRIPTOR)}
|
||||
</h3>
|
||||
<p className={previewStyles.endStateDescription} data-flx="messaging.saved-messages-content.p--2">
|
||||
{i18n._(THERE_S_NOTHING_MORE_TO_SEE_HERE_DESCRIPTOR)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
</Scroller>
|
||||
</NearViewportSurfaceContext.Provider>
|
||||
);
|
||||
});
|
||||
@@ -2,14 +2,27 @@
|
||||
|
||||
import {observeIntersection} from '@app/features/platform/utils/SharedIntersectionObserver';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {createContext, useCallback, useContext, useEffect, useState} from 'react';
|
||||
|
||||
const DEFAULT_ROOT_MARGIN = '900px 0px';
|
||||
const DEFAULT_ROOT_MARGIN = '300px';
|
||||
const REMEMBERED_VIEWPORT_KEY_LIMIT = 1000;
|
||||
const rememberedViewportKeys = new LRUCache<string, true>({
|
||||
max: REMEMBERED_VIEWPORT_KEY_LIMIT,
|
||||
});
|
||||
|
||||
export type ScrollSurfaceResolver = () => Element | null;
|
||||
|
||||
const resolveNoScrollSurface: ScrollSurfaceResolver = () => null;
|
||||
|
||||
export const NearViewportSurfaceContext = createContext<ScrollSurfaceResolver>(resolveNoScrollSurface);
|
||||
|
||||
export function resolveObserverRoot(resolve: ScrollSurfaceResolver, element: Element): Element | null {
|
||||
const surface = resolve();
|
||||
if (surface == null || surface === element) return null;
|
||||
if (!surface.contains(element)) return null;
|
||||
return surface;
|
||||
}
|
||||
|
||||
interface UseNearViewportOptions {
|
||||
disabled?: boolean;
|
||||
rememberKey?: string | null;
|
||||
@@ -23,6 +36,7 @@ export function useNearViewport<T extends Element>({
|
||||
rootMargin = DEFAULT_ROOT_MARGIN,
|
||||
threshold = 0,
|
||||
}: UseNearViewportOptions = {}): {ref: (node: T | null) => void; isNearViewport: boolean} {
|
||||
const resolveScrollSurface = useContext(NearViewportSurfaceContext);
|
||||
const loadImmediately = disabled || typeof IntersectionObserver === 'undefined';
|
||||
const wasRemembered = rememberKey ? rememberedViewportKeys.has(rememberKey) : false;
|
||||
const [element, setElement] = useState<T | null>(null);
|
||||
@@ -36,24 +50,34 @@ export function useNearViewport<T extends Element>({
|
||||
}
|
||||
}, [disabled]);
|
||||
useEffect(() => {
|
||||
if (isNearViewport && rememberKey) {
|
||||
rememberedViewportKeys.set(rememberKey, true);
|
||||
}
|
||||
}, [isNearViewport, rememberKey]);
|
||||
if (!isNearViewport || !rememberKey) return;
|
||||
if (disabled || typeof IntersectionObserver === 'undefined') return;
|
||||
rememberedViewportKeys.set(rememberKey, true);
|
||||
}, [disabled, isNearViewport, rememberKey]);
|
||||
useEffect(() => {
|
||||
if (disabled || isNearViewport || !element) return undefined;
|
||||
return observeIntersection(
|
||||
if (rememberKey && rememberedViewportKeys.has(rememberKey)) {
|
||||
setIsNearViewport(true);
|
||||
return undefined;
|
||||
}
|
||||
let release: (() => void) | null = null;
|
||||
const stopObserving = () => {
|
||||
release?.();
|
||||
release = null;
|
||||
};
|
||||
release = observeIntersection(
|
||||
element,
|
||||
(entry) => {
|
||||
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
||||
if (rememberKey) {
|
||||
rememberedViewportKeys.set(rememberKey, true);
|
||||
}
|
||||
setIsNearViewport(true);
|
||||
if (!entry.isIntersecting && entry.intersectionRatio <= 0) return;
|
||||
stopObserving();
|
||||
if (rememberKey) {
|
||||
rememberedViewportKeys.set(rememberKey, true);
|
||||
}
|
||||
setIsNearViewport(true);
|
||||
},
|
||||
{rootMargin, threshold},
|
||||
{root: resolveObserverRoot(resolveScrollSurface, element), rootMargin, threshold},
|
||||
);
|
||||
}, [disabled, element, isNearViewport, rememberKey, rootMargin, threshold]);
|
||||
return stopObserving;
|
||||
}, [disabled, element, isNearViewport, rememberKey, resolveScrollSurface, rootMargin, threshold]);
|
||||
return {ref, isNearViewport};
|
||||
}
|
||||
Reference in new issue
Block a user