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