perf(app): load sheet and popout media against their own scrollers (#1900)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:42:09 +02:00
1 parent 94bbbd1021
commit bb34c73069
3 files changed
+217 -200

No files matched your search

@@ -27,6 +27,7 @@ import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
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 {
applyChannelSearchHighlight,
@@ -67,7 +68,7 @@ import {MessagePreviewContext} from '@fluxer/constants/src/ChannelConstants';
import {Trans, useLingui} from '@lingui/react/macro';
import {HashIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
const NEWEST_DESCRIPTOR = msg({
message: 'Newest',
@@ -189,6 +190,7 @@ export const ChannelSearchBottomSheet: React.FC<ChannelSearchBottomSheetProps> =
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
const [selectedMessageChannel, setSelectedMessageChannel] = useState<Channel | null>(null);
const scrollerRef = useRef<ScrollerHandle | null>(null);
const resolveSearchSheetScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
const inputRef = useRef<HTMLInputElement>(null);
const [hasFilters, setHasFilters] = useState<Array<HasFilterType>>([]);
const [fromUserIds, setFromUserIds] = useState<Array<string>>([]);
@@ -605,89 +607,91 @@ export const ChannelSearchBottomSheet: React.FC<ChannelSearchBottomSheetProps> =
value={collapsedMessageVisibility}
data-flx="channel.channel-search-bottom-sheet.render-content.collapsed-message-visibility-provider"
>
<Scroller
ref={scrollerRef}
className={styles.resultsScroller}
key="channel-search-results-scroller"
onCopy={onCopySelectedMessages}
data-message-selection-root="true"
data-flx="channel.channel-search-bottom-sheet.render-content.results-scroller"
>
{resultGroups.map((resultGroup) => {
const messageChannel =
searchChannelsById.get(resultGroup.channelId) ?? Channels.getChannel(resultGroup.channelId);
if (!messageChannel) {
return null;
}
const renderMessageWrapper = ({
message,
index,
isGroupStart,
children,
}: MessageGroupRenderWrapperProps) => (
<LongPressable
data-message-index={index}
data-message-id={message.id}
data-is-group-start={isGroupStart}
className={styles.searchResultItem}
role="button"
tabIndex={0}
onClick={() => handleTap(message)}
onKeyDown={(e) => {
if (isKeyboardActivationKey(e.key)) {
e.preventDefault();
handleTap(message);
}
}}
onLongPress={() => {
setSelectedMessage(message);
setMenuOpen(true);
}}
data-flx="channel.channel-search-bottom-sheet.render-message-wrapper.search-result-item.tap"
>
{children}
</LongPressable>
);
return (
<React.Fragment key={resultGroup.key}>
{hasMultipleChannels && (
<div
className={styles.channelSection}
data-flx="channel.channel-search-bottom-sheet.render-content.channel-section"
>
{ChannelUtils.getIcon(messageChannel, {
className: styles.channelIcon,
})}
<span
className={styles.channelName}
data-flx="channel.channel-search-bottom-sheet.render-content.channel-name"
<NearViewportSurfaceContext.Provider value={resolveSearchSheetScrollSurface}>
<Scroller
ref={scrollerRef}
className={styles.resultsScroller}
key="channel-search-results-scroller"
onCopy={onCopySelectedMessages}
data-message-selection-root="true"
data-flx="channel.channel-search-bottom-sheet.render-content.results-scroller"
>
{resultGroups.map((resultGroup) => {
const messageChannel =
searchChannelsById.get(resultGroup.channelId) ?? Channels.getChannel(resultGroup.channelId);
if (!messageChannel) {
return null;
}
const renderMessageWrapper = ({
message,
index,
isGroupStart,
children,
}: MessageGroupRenderWrapperProps) => (
<LongPressable
data-message-index={index}
data-message-id={message.id}
data-is-group-start={isGroupStart}
className={styles.searchResultItem}
role="button"
tabIndex={0}
onClick={() => handleTap(message)}
onKeyDown={(e) => {
if (isKeyboardActivationKey(e.key)) {
e.preventDefault();
handleTap(message);
}
}}
onLongPress={() => {
setSelectedMessage(message);
setMenuOpen(true);
}}
data-flx="channel.channel-search-bottom-sheet.render-message-wrapper.search-result-item.tap"
>
{children}
</LongPressable>
);
return (
<React.Fragment key={resultGroup.key}>
{hasMultipleChannels && (
<div
className={styles.channelSection}
data-flx="channel.channel-search-bottom-sheet.render-content.channel-section"
>
{messageChannel.name || 'Unnamed Channel'}
</span>
</div>
)}
<SearchResultMessageList
channel={messageChannel}
messages={resultGroup.messages}
revealedGroupKeys={revealedGroupKeys}
onGroupRevealChange={handleCollapsedGroupRevealChange}
collapsedGroupClassName={styles.collapsedMessageGroup}
messagePreviewContext={MessagePreviewContext.LIST_POPOUT}
renderMessageWrapper={renderMessageWrapper}
spammerOverrideVersion={spammerOverrideVersion}
renderMessage={(message) => (
<SearchResultItem
message={message}
messageChannel={messageChannel}
data-flx="channel.channel-search-bottom-sheet.render-content.search-result-item"
/>
{ChannelUtils.getIcon(messageChannel, {
className: styles.channelIcon,
})}
<span
className={styles.channelName}
data-flx="channel.channel-search-bottom-sheet.render-content.channel-name"
>
{messageChannel.name || 'Unnamed Channel'}
</span>
</div>
)}
data-flx="channel.channel-search-bottom-sheet.render-content.search-result-message-list"
/>
</React.Fragment>
);
})}
</Scroller>
<SearchResultMessageList
channel={messageChannel}
messages={resultGroup.messages}
revealedGroupKeys={revealedGroupKeys}
onGroupRevealChange={handleCollapsedGroupRevealChange}
collapsedGroupClassName={styles.collapsedMessageGroup}
messagePreviewContext={MessagePreviewContext.LIST_POPOUT}
renderMessageWrapper={renderMessageWrapper}
spammerOverrideVersion={spammerOverrideVersion}
renderMessage={(message) => (
<SearchResultItem
message={message}
messageChannel={messageChannel}
data-flx="channel.channel-search-bottom-sheet.render-content.search-result-item"
/>
)}
data-flx="channel.channel-search-bottom-sheet.render-content.search-result-message-list"
/>
</React.Fragment>
);
})}
</Scroller>
</NearViewportSurfaceContext.Provider>
</CollapsedMessageVisibilityProvider>
{totalPages > 1 && (
<div
@@ -10,6 +10,7 @@ import {ensureMembersForMessages} from '@app/features/messaging/commands/Message
import * as SavedMessageCommands from '@app/features/messaging/commands/SavedMessageCommands';
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 SavedMessages from '@app/features/messaging/state/SavedMessages';
import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils';
@@ -23,7 +24,7 @@ import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
import {ArrowSquareOutIcon, TrashIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import {useEffect, useRef, useState} from 'react';
import {useEffect, useMemo, useRef, useState} from 'react';
const JUMP_TO_MESSAGE_DESCRIPTOR = msg({
message: 'Jump to message',
@@ -44,6 +45,7 @@ export const BookmarksBottomSheet = observer(({isOpen, onClose}: BookmarksBottom
const {savedMessages, missingSavedMessages, fetched} = SavedMessages;
const hasBookmarks = savedMessages.length > 0 || missingSavedMessages.length > 0;
const scrollerRef = useRef<ScrollerHandle | null>(null);
const resolveBookmarksScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
const [menuOpen, setMenuOpen] = useState(false);
const onCopySelectedMessages = useMessageSelectionCopyForMessages<HTMLDivElement>(savedMessages);
@@ -122,39 +124,41 @@ export const BookmarksBottomSheet = observer(({isOpen, onClose}: BookmarksBottom
data-flx="channel.bookmarks-bottom-sheet.bottom-sheet"
>
{hasBookmarks ? (
<Scroller
className={styles.messageList}
key="bookmarks-bottom-sheet-scroller"
ref={scrollerRef}
onCopy={onCopySelectedMessages}
data-message-selection-root="true"
data-flx="channel.bookmarks-bottom-sheet.message-list"
>
{missingSavedMessages.length > 0 && (
<div className={styles.missingList} data-flx="channel.bookmarks-bottom-sheet.missing-list">
{missingSavedMessages.map((entry) => (
<SavedMessageMissingCard
key={entry.id}
entryId={entry.id}
onRemove={() => SavedMessageCommands.remove(i18n, entry.id)}
data-flx="channel.bookmarks-bottom-sheet.saved-message-missing-card"
<NearViewportSurfaceContext.Provider value={resolveBookmarksScrollSurface}>
<Scroller
className={styles.messageList}
key="bookmarks-bottom-sheet-scroller"
ref={scrollerRef}
onCopy={onCopySelectedMessages}
data-message-selection-root="true"
data-flx="channel.bookmarks-bottom-sheet.message-list"
>
{missingSavedMessages.length > 0 && (
<div className={styles.missingList} data-flx="channel.bookmarks-bottom-sheet.missing-list">
{missingSavedMessages.map((entry) => (
<SavedMessageMissingCard
key={entry.id}
entryId={entry.id}
onRemove={() => SavedMessageCommands.remove(i18n, entry.id)}
data-flx="channel.bookmarks-bottom-sheet.saved-message-missing-card"
/>
))}
</div>
)}
<div className={styles.topSpacer} data-flx="channel.bookmarks-bottom-sheet.top-spacer" />
<div className={styles.messagesContainer} data-flx="channel.bookmarks-bottom-sheet.messages-container">
{savedMessages.map((message) => (
<MessageWithLongPress
key={message.id}
message={message}
onLongPress={handleLongPress}
onClick={handleJumpToMessage}
data-flx="channel.bookmarks-bottom-sheet.message-with-long-press.jump-to-message"
/>
))}
</div>
)}
<div className={styles.topSpacer} data-flx="channel.bookmarks-bottom-sheet.top-spacer" />
<div className={styles.messagesContainer} data-flx="channel.bookmarks-bottom-sheet.messages-container">
{savedMessages.map((message) => (
<MessageWithLongPress
key={message.id}
message={message}
onLongPress={handleLongPress}
onClick={handleJumpToMessage}
data-flx="channel.bookmarks-bottom-sheet.message-with-long-press.jump-to-message"
/>
))}
</div>
</Scroller>
</Scroller>
</NearViewportSurfaceContext.Provider>
) : (
<div className={styles.emptyState} data-flx="channel.bookmarks-bottom-sheet.empty-state">
<div className={styles.emptyContent} data-flx="channel.bookmarks-bottom-sheet.empty-content">