mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
perf(app): load sheet and popout media against their own scrollers (#1900)
This commit is contained in:
1 parent
94bbbd1021
commit
bb34c73069
3 files changed
+217
-200
No files matched your search
@@ -13,6 +13,7 @@ import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
|||||||
import {ensureMembersForMessages} from '@app/features/messaging/commands/MessageCommands';
|
import {ensureMembersForMessages} from '@app/features/messaging/commands/MessageCommands';
|
||||||
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';
|
||||||
@@ -79,6 +80,7 @@ export const ChannelPinsContent = observer(({channel, onJump}: ChannelPinsConten
|
|||||||
const canUnpin = isDMChannel || Permission.can(Permissions.MANAGE_MESSAGES, channel);
|
const canUnpin = isDMChannel || Permission.can(Permissions.MANAGE_MESSAGES, channel);
|
||||||
const mobileLayout = MobileLayout;
|
const mobileLayout = MobileLayout;
|
||||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||||
|
const resolvePinnedScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
|
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
|
||||||
const pinMessagesPermissionLabel = formatPermissionLabel(i18n, Permissions.PIN_MESSAGES);
|
const pinMessagesPermissionLabel = formatPermissionLabel(i18n, Permissions.PIN_MESSAGES);
|
||||||
@@ -187,107 +189,114 @@ export const ChannelPinsContent = observer(({channel, onJump}: ChannelPinsConten
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Scroller
|
<NearViewportSurfaceContext.Provider value={resolvePinnedScrollSurface}>
|
||||||
className={clsx(previewStyles.scroller, mobileLayout.enabled && previewStyles.scrollerMobile)}
|
<Scroller
|
||||||
key="channel-pins-scroller"
|
className={clsx(previewStyles.scroller, mobileLayout.enabled && previewStyles.scrollerMobile)}
|
||||||
onScroll={handleScroll}
|
key="channel-pins-scroller"
|
||||||
ref={scrollerRef}
|
onScroll={handleScroll}
|
||||||
onCopy={onCopySelectedMessages}
|
ref={scrollerRef}
|
||||||
data-message-selection-root="true"
|
onCopy={onCopySelectedMessages}
|
||||||
data-flx="app.channel-pins-content.scroller"
|
data-message-selection-root="true"
|
||||||
>
|
data-flx="app.channel-pins-content.scroller"
|
||||||
{mobileLayout.enabled && <div className={previewStyles.topSpacer} data-flx="app.channel-pins-content.div--5" />}
|
>
|
||||||
{pinnedPins.slice().map(({message}) => {
|
{mobileLayout.enabled && (
|
||||||
const cardClasses = clsx(previewStyles.previewCard, mobileLayout.enabled && previewStyles.previewCardMobile);
|
<div className={previewStyles.topSpacer} data-flx="app.channel-pins-content.div--5" />
|
||||||
const messageChannel =
|
)}
|
||||||
Channels.getChannel(message.channelId) ?? (message.channelId === channel.id ? channel : null);
|
{pinnedPins.slice().map(({message}) => {
|
||||||
if (!messageChannel) return null;
|
const cardClasses = clsx(
|
||||||
if (mobileLayout.enabled) {
|
previewStyles.previewCard,
|
||||||
|
mobileLayout.enabled && previewStyles.previewCardMobile,
|
||||||
|
);
|
||||||
|
const messageChannel =
|
||||||
|
Channels.getChannel(message.channelId) ?? (message.channelId === channel.id ? channel : null);
|
||||||
|
if (!messageChannel) return null;
|
||||||
|
if (mobileLayout.enabled) {
|
||||||
|
return (
|
||||||
|
<LongPressable
|
||||||
|
key={message.id}
|
||||||
|
className={cardClasses}
|
||||||
|
data-message-id={message.id}
|
||||||
|
data-is-group-start="true"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => handleTap(message)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (isKeyboardActivationKey(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTap(message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onLongPress={() => {
|
||||||
|
if (!canUnpin) return;
|
||||||
|
setSelectedMessage(message);
|
||||||
|
setMenuOpen(true);
|
||||||
|
}}
|
||||||
|
data-flx="app.channel-pins-content.button.tap"
|
||||||
|
>
|
||||||
|
<MessageComponent
|
||||||
|
message={message}
|
||||||
|
channel={messageChannel}
|
||||||
|
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||||
|
data-flx="app.channel-pins-content.message-component"
|
||||||
|
/>
|
||||||
|
</LongPressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<LongPressable
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={cardClasses}
|
className={cardClasses}
|
||||||
data-message-id={message.id}
|
data-message-id={message.id}
|
||||||
data-is-group-start="true"
|
data-is-group-start="true"
|
||||||
role="button"
|
data-flx="app.channel-pins-content.div--6"
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => handleTap(message)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (isKeyboardActivationKey(e.key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleTap(message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onLongPress={() => {
|
|
||||||
if (!canUnpin) return;
|
|
||||||
setSelectedMessage(message);
|
|
||||||
setMenuOpen(true);
|
|
||||||
}}
|
|
||||||
data-flx="app.channel-pins-content.button.tap"
|
|
||||||
>
|
>
|
||||||
<MessageComponent
|
<MessageComponent
|
||||||
message={message}
|
message={message}
|
||||||
channel={messageChannel}
|
channel={messageChannel}
|
||||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||||
data-flx="app.channel-pins-content.message-component"
|
data-flx="app.channel-pins-content.message-component--2"
|
||||||
/>
|
/>
|
||||||
</LongPressable>
|
<div className={previewStyles.actionButtons} data-flx="app.channel-pins-content.div--7">
|
||||||
|
<FocusRing offset={-2} data-flx="app.channel-pins-content.focus-ring">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={previewStyles.actionButton}
|
||||||
|
onClick={() => handleJump(message.channelId, message.id)}
|
||||||
|
data-flx="app.channel-pins-content.button.jump"
|
||||||
|
>
|
||||||
|
{i18n._(JUMP_DESCRIPTOR)}
|
||||||
|
</button>
|
||||||
|
</FocusRing>
|
||||||
|
{renderUnpinButton(message)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
return (
|
{isLoading && (
|
||||||
<div
|
<div className={previewStyles.loadingState} data-flx="app.channel-pins-content.div--8">
|
||||||
key={message.id}
|
<Spinner data-flx="app.channel-pins-content.spinner--2" />
|
||||||
className={cardClasses}
|
</div>
|
||||||
data-message-id={message.id}
|
)}
|
||||||
data-is-group-start="true"
|
{!hasMore && (
|
||||||
data-flx="app.channel-pins-content.div--6"
|
<div className={previewStyles.endState} data-flx="app.channel-pins-content.div--9">
|
||||||
>
|
<div className={previewStyles.endStateContent} data-flx="app.channel-pins-content.div--10">
|
||||||
<MessageComponent
|
<FlagCheckeredIcon
|
||||||
message={message}
|
className={previewStyles.endStateIcon}
|
||||||
channel={messageChannel}
|
data-flx="app.channel-pins-content.flag-checkered-icon"
|
||||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
/>
|
||||||
data-flx="app.channel-pins-content.message-component--2"
|
<div className={previewStyles.endStateTextContainer} data-flx="app.channel-pins-content.div--11">
|
||||||
/>
|
<h3 className={previewStyles.endStateTitle} data-flx="app.channel-pins-content.h3--2">
|
||||||
<div className={previewStyles.actionButtons} data-flx="app.channel-pins-content.div--7">
|
{i18n._(YOU_VE_REACHED_THE_END_DESCRIPTOR)}
|
||||||
<FocusRing offset={-2} data-flx="app.channel-pins-content.focus-ring">
|
</h3>
|
||||||
<button
|
<p className={previewStyles.endStateDescription} data-flx="app.channel-pins-content.p--2">
|
||||||
type="button"
|
{endStateDescription}
|
||||||
className={previewStyles.actionButton}
|
</p>
|
||||||
onClick={() => handleJump(message.channelId, message.id)}
|
</div>
|
||||||
data-flx="app.channel-pins-content.button.jump"
|
|
||||||
>
|
|
||||||
{i18n._(JUMP_DESCRIPTOR)}
|
|
||||||
</button>
|
|
||||||
</FocusRing>
|
|
||||||
{renderUnpinButton(message)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
</Scroller>
|
||||||
{isLoading && (
|
</NearViewportSurfaceContext.Provider>
|
||||||
<div className={previewStyles.loadingState} data-flx="app.channel-pins-content.div--8">
|
|
||||||
<Spinner data-flx="app.channel-pins-content.spinner--2" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!hasMore && (
|
|
||||||
<div className={previewStyles.endState} data-flx="app.channel-pins-content.div--9">
|
|
||||||
<div className={previewStyles.endStateContent} data-flx="app.channel-pins-content.div--10">
|
|
||||||
<FlagCheckeredIcon
|
|
||||||
className={previewStyles.endStateIcon}
|
|
||||||
data-flx="app.channel-pins-content.flag-checkered-icon"
|
|
||||||
/>
|
|
||||||
<div className={previewStyles.endStateTextContainer} data-flx="app.channel-pins-content.div--11">
|
|
||||||
<h3 className={previewStyles.endStateTitle} data-flx="app.channel-pins-content.h3--2">
|
|
||||||
{i18n._(YOU_VE_REACHED_THE_END_DESCRIPTOR)}
|
|
||||||
</h3>
|
|
||||||
<p className={previewStyles.endStateDescription} data-flx="app.channel-pins-content.p--2">
|
|
||||||
{endStateDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Scroller>
|
|
||||||
{mobileLayout.enabled && selectedMessage && (
|
{mobileLayout.enabled && selectedMessage && (
|
||||||
<MenuBottomSheet
|
<MenuBottomSheet
|
||||||
isOpen={menuOpen}
|
isOpen={menuOpen}
|
||||||
|
|||||||
+86
-82
@@ -27,6 +27,7 @@ import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
|||||||
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
|
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
|
||||||
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 {
|
import {
|
||||||
applyChannelSearchHighlight,
|
applyChannelSearchHighlight,
|
||||||
@@ -67,7 +68,7 @@ import {MessagePreviewContext} from '@fluxer/constants/src/ChannelConstants';
|
|||||||
import {Trans, useLingui} from '@lingui/react/macro';
|
import {Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {HashIcon} from '@phosphor-icons/react';
|
import {HashIcon} from '@phosphor-icons/react';
|
||||||
import {observer} from 'mobx-react-lite';
|
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({
|
const NEWEST_DESCRIPTOR = msg({
|
||||||
message: 'Newest',
|
message: 'Newest',
|
||||||
@@ -189,6 +190,7 @@ export const ChannelSearchBottomSheet: React.FC<ChannelSearchBottomSheetProps> =
|
|||||||
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
|
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
|
||||||
const [selectedMessageChannel, setSelectedMessageChannel] = useState<Channel | null>(null);
|
const [selectedMessageChannel, setSelectedMessageChannel] = useState<Channel | null>(null);
|
||||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||||
|
const resolveSearchSheetScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [hasFilters, setHasFilters] = useState<Array<HasFilterType>>([]);
|
const [hasFilters, setHasFilters] = useState<Array<HasFilterType>>([]);
|
||||||
const [fromUserIds, setFromUserIds] = useState<Array<string>>([]);
|
const [fromUserIds, setFromUserIds] = useState<Array<string>>([]);
|
||||||
@@ -605,89 +607,91 @@ export const ChannelSearchBottomSheet: React.FC<ChannelSearchBottomSheetProps> =
|
|||||||
value={collapsedMessageVisibility}
|
value={collapsedMessageVisibility}
|
||||||
data-flx="channel.channel-search-bottom-sheet.render-content.collapsed-message-visibility-provider"
|
data-flx="channel.channel-search-bottom-sheet.render-content.collapsed-message-visibility-provider"
|
||||||
>
|
>
|
||||||
<Scroller
|
<NearViewportSurfaceContext.Provider value={resolveSearchSheetScrollSurface}>
|
||||||
ref={scrollerRef}
|
<Scroller
|
||||||
className={styles.resultsScroller}
|
ref={scrollerRef}
|
||||||
key="channel-search-results-scroller"
|
className={styles.resultsScroller}
|
||||||
onCopy={onCopySelectedMessages}
|
key="channel-search-results-scroller"
|
||||||
data-message-selection-root="true"
|
onCopy={onCopySelectedMessages}
|
||||||
data-flx="channel.channel-search-bottom-sheet.render-content.results-scroller"
|
data-message-selection-root="true"
|
||||||
>
|
data-flx="channel.channel-search-bottom-sheet.render-content.results-scroller"
|
||||||
{resultGroups.map((resultGroup) => {
|
>
|
||||||
const messageChannel =
|
{resultGroups.map((resultGroup) => {
|
||||||
searchChannelsById.get(resultGroup.channelId) ?? Channels.getChannel(resultGroup.channelId);
|
const messageChannel =
|
||||||
if (!messageChannel) {
|
searchChannelsById.get(resultGroup.channelId) ?? Channels.getChannel(resultGroup.channelId);
|
||||||
return null;
|
if (!messageChannel) {
|
||||||
}
|
return null;
|
||||||
const renderMessageWrapper = ({
|
}
|
||||||
message,
|
const renderMessageWrapper = ({
|
||||||
index,
|
message,
|
||||||
isGroupStart,
|
index,
|
||||||
children,
|
isGroupStart,
|
||||||
}: MessageGroupRenderWrapperProps) => (
|
children,
|
||||||
<LongPressable
|
}: MessageGroupRenderWrapperProps) => (
|
||||||
data-message-index={index}
|
<LongPressable
|
||||||
data-message-id={message.id}
|
data-message-index={index}
|
||||||
data-is-group-start={isGroupStart}
|
data-message-id={message.id}
|
||||||
className={styles.searchResultItem}
|
data-is-group-start={isGroupStart}
|
||||||
role="button"
|
className={styles.searchResultItem}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onClick={() => handleTap(message)}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => {
|
onClick={() => handleTap(message)}
|
||||||
if (isKeyboardActivationKey(e.key)) {
|
onKeyDown={(e) => {
|
||||||
e.preventDefault();
|
if (isKeyboardActivationKey(e.key)) {
|
||||||
handleTap(message);
|
e.preventDefault();
|
||||||
}
|
handleTap(message);
|
||||||
}}
|
}
|
||||||
onLongPress={() => {
|
}}
|
||||||
setSelectedMessage(message);
|
onLongPress={() => {
|
||||||
setMenuOpen(true);
|
setSelectedMessage(message);
|
||||||
}}
|
setMenuOpen(true);
|
||||||
data-flx="channel.channel-search-bottom-sheet.render-message-wrapper.search-result-item.tap"
|
}}
|
||||||
>
|
data-flx="channel.channel-search-bottom-sheet.render-message-wrapper.search-result-item.tap"
|
||||||
{children}
|
>
|
||||||
</LongPressable>
|
{children}
|
||||||
);
|
</LongPressable>
|
||||||
return (
|
);
|
||||||
<React.Fragment key={resultGroup.key}>
|
return (
|
||||||
{hasMultipleChannels && (
|
<React.Fragment key={resultGroup.key}>
|
||||||
<div
|
{hasMultipleChannels && (
|
||||||
className={styles.channelSection}
|
<div
|
||||||
data-flx="channel.channel-search-bottom-sheet.render-content.channel-section"
|
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"
|
|
||||||
>
|
>
|
||||||
{messageChannel.name || 'Unnamed Channel'}
|
{ChannelUtils.getIcon(messageChannel, {
|
||||||
</span>
|
className: styles.channelIcon,
|
||||||
</div>
|
})}
|
||||||
)}
|
<span
|
||||||
<SearchResultMessageList
|
className={styles.channelName}
|
||||||
channel={messageChannel}
|
data-flx="channel.channel-search-bottom-sheet.render-content.channel-name"
|
||||||
messages={resultGroup.messages}
|
>
|
||||||
revealedGroupKeys={revealedGroupKeys}
|
{messageChannel.name || 'Unnamed Channel'}
|
||||||
onGroupRevealChange={handleCollapsedGroupRevealChange}
|
</span>
|
||||||
collapsedGroupClassName={styles.collapsedMessageGroup}
|
</div>
|
||||||
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"
|
<SearchResultMessageList
|
||||||
/>
|
channel={messageChannel}
|
||||||
</React.Fragment>
|
messages={resultGroup.messages}
|
||||||
);
|
revealedGroupKeys={revealedGroupKeys}
|
||||||
})}
|
onGroupRevealChange={handleCollapsedGroupRevealChange}
|
||||||
</Scroller>
|
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>
|
</CollapsedMessageVisibilityProvider>
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {ensureMembersForMessages} from '@app/features/messaging/commands/Message
|
|||||||
import * as SavedMessageCommands from '@app/features/messaging/commands/SavedMessageCommands';
|
import * as SavedMessageCommands from '@app/features/messaging/commands/SavedMessageCommands';
|
||||||
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 SavedMessages from '@app/features/messaging/state/SavedMessages';
|
import SavedMessages from '@app/features/messaging/state/SavedMessages';
|
||||||
import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils';
|
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 {Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {ArrowSquareOutIcon, TrashIcon} from '@phosphor-icons/react';
|
import {ArrowSquareOutIcon, TrashIcon} from '@phosphor-icons/react';
|
||||||
import {observer} from 'mobx-react-lite';
|
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({
|
const JUMP_TO_MESSAGE_DESCRIPTOR = msg({
|
||||||
message: 'Jump to message',
|
message: 'Jump to message',
|
||||||
@@ -44,6 +45,7 @@ export const BookmarksBottomSheet = observer(({isOpen, onClose}: BookmarksBottom
|
|||||||
const {savedMessages, missingSavedMessages, fetched} = SavedMessages;
|
const {savedMessages, missingSavedMessages, fetched} = SavedMessages;
|
||||||
const hasBookmarks = savedMessages.length > 0 || missingSavedMessages.length > 0;
|
const hasBookmarks = savedMessages.length > 0 || missingSavedMessages.length > 0;
|
||||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||||
|
const resolveBookmarksScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||||
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
|
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const onCopySelectedMessages = useMessageSelectionCopyForMessages<HTMLDivElement>(savedMessages);
|
const onCopySelectedMessages = useMessageSelectionCopyForMessages<HTMLDivElement>(savedMessages);
|
||||||
@@ -122,39 +124,41 @@ export const BookmarksBottomSheet = observer(({isOpen, onClose}: BookmarksBottom
|
|||||||
data-flx="channel.bookmarks-bottom-sheet.bottom-sheet"
|
data-flx="channel.bookmarks-bottom-sheet.bottom-sheet"
|
||||||
>
|
>
|
||||||
{hasBookmarks ? (
|
{hasBookmarks ? (
|
||||||
<Scroller
|
<NearViewportSurfaceContext.Provider value={resolveBookmarksScrollSurface}>
|
||||||
className={styles.messageList}
|
<Scroller
|
||||||
key="bookmarks-bottom-sheet-scroller"
|
className={styles.messageList}
|
||||||
ref={scrollerRef}
|
key="bookmarks-bottom-sheet-scroller"
|
||||||
onCopy={onCopySelectedMessages}
|
ref={scrollerRef}
|
||||||
data-message-selection-root="true"
|
onCopy={onCopySelectedMessages}
|
||||||
data-flx="channel.bookmarks-bottom-sheet.message-list"
|
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.length > 0 && (
|
||||||
{missingSavedMessages.map((entry) => (
|
<div className={styles.missingList} data-flx="channel.bookmarks-bottom-sheet.missing-list">
|
||||||
<SavedMessageMissingCard
|
{missingSavedMessages.map((entry) => (
|
||||||
key={entry.id}
|
<SavedMessageMissingCard
|
||||||
entryId={entry.id}
|
key={entry.id}
|
||||||
onRemove={() => SavedMessageCommands.remove(i18n, entry.id)}
|
entryId={entry.id}
|
||||||
data-flx="channel.bookmarks-bottom-sheet.saved-message-missing-card"
|
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>
|
||||||
)}
|
</Scroller>
|
||||||
<div className={styles.topSpacer} data-flx="channel.bookmarks-bottom-sheet.top-spacer" />
|
</NearViewportSurfaceContext.Provider>
|
||||||
<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>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.emptyState} data-flx="channel.bookmarks-bottom-sheet.empty-state">
|
<div className={styles.emptyState} data-flx="channel.bookmarks-bottom-sheet.empty-state">
|
||||||
<div className={styles.emptyContent} data-flx="channel.bookmarks-bottom-sheet.empty-content">
|
<div className={styles.emptyContent} data-flx="channel.bookmarks-bottom-sheet.empty-content">
|
||||||
|
|||||||
Reference in new issue
Block a user