mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
perf(app): stop preloading channels and guilds on hover (#3133)
This commit is contained in:
@@ -21,7 +21,6 @@ import {type DragItem, DragItemType, type DropResult} from '@app/features/app/co
|
||||
import {isCategory, isTextChannel} from '@app/features/app/components/layout/utils/ChannelOrganization';
|
||||
import {getChannelUnreadState} from '@app/features/app/components/layout/utils/ChannelUnreadState';
|
||||
import {VoiceChannelUserCount} from '@app/features/app/components/layout/VoiceChannelUserCount';
|
||||
import {useChannelHoverPreload} from '@app/features/app/hooks/useChannelHoverPreload';
|
||||
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
|
||||
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
|
||||
import {useTextOverflow} from '@app/features/app/hooks/useTextOverflow';
|
||||
@@ -220,12 +219,6 @@ export const ChannelItem = observer(
|
||||
const channelIsCategory = isCategory(channel);
|
||||
const channelIsVoice = channelType === ChannelTypes.GUILD_VOICE;
|
||||
const channelIsText = isTextChannel(channel);
|
||||
const {scheduleChannelPreload, cancelChannelPreload, preloadChannelNow} = useChannelHoverPreload({
|
||||
channel,
|
||||
guild,
|
||||
defaultHiddenForChannel: channelIsVoice,
|
||||
enabled: !channelIsCategory,
|
||||
});
|
||||
const draggingChannel = activeDragItem?.type === DragItemType.CHANNEL ? activeDragItem : null;
|
||||
const isVoiceDragActive = draggingChannel?.channelType === ChannelTypes.GUILD_VOICE;
|
||||
const shouldDimForVoiceDrag = Boolean(isVoiceDragActive && channelIsText && channel.parentId !== null);
|
||||
@@ -401,12 +394,11 @@ export const ChannelItem = observer(
|
||||
const singleClickConnectsToVoice =
|
||||
channelIsVoice && !Accessibility.voiceChannelJoinRequiresDoubleClick && !isVoiceSelected;
|
||||
const navigateToChannel = useCallback(() => {
|
||||
preloadChannelNow();
|
||||
NavigationCommands.selectChannel(guild.id, channel.id);
|
||||
if (MobileLayout.isMobileLayout()) {
|
||||
LayoutCommands.updateMobileLayoutState(false, true);
|
||||
}
|
||||
}, [guild.id, channel.id, preloadChannelNow]);
|
||||
}, [guild.id, channel.id]);
|
||||
const collapseVoiceCallView = useCallback(() => {
|
||||
if (!channelIsVoice) return;
|
||||
CompactVoiceCallHeight.setExpandedForKey(getGuildVoiceCallExpansionKey(channel.id), false);
|
||||
@@ -504,12 +496,10 @@ export const ChannelItem = observer(
|
||||
const [isPointerHovered, setIsPointerHovered] = useState(false);
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsPointerHovered(true);
|
||||
scheduleChannelPreload();
|
||||
}, [scheduleChannelPreload]);
|
||||
}, []);
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsPointerHovered(false);
|
||||
cancelChannelPreload();
|
||||
}, [cancelChannelPreload]);
|
||||
}, []);
|
||||
const hoverAffordancesActive =
|
||||
allowHoverAffordances &&
|
||||
(contextMenuOpen || showKeyboardAffordances || shouldShowSelectedState || isPointerHovered);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {DragItemType} from '@app/features/app/components/layout/types/DndTypes';
|
||||
import {getChannelUnreadState} from '@app/features/app/components/layout/utils/ChannelUnreadState';
|
||||
import {GroupDMAvatar} from '@app/features/app/components/shared/GroupDMAvatar';
|
||||
import {GuildChannelListSkeleton} from '@app/features/app/components/skeleton/GuildSidebarSkeleton';
|
||||
import {useChannelHoverPreload} from '@app/features/app/hooks/useChannelHoverPreload';
|
||||
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
|
||||
import * as LinkChannelCommands from '@app/features/channel/commands/LinkChannelCommands';
|
||||
import type {Channel} from '@app/features/channel/models/Channel';
|
||||
@@ -55,7 +54,6 @@ import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||
import UserGuildSettings from '@app/features/user/state/UserGuildSettings';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import {FAVORITES_GUILD_ID, ME} from '@fluxer/constants/src/AppConstants';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {CaretDownIcon, PlusIcon, UserPlusIcon} from '@phosphor-icons/react';
|
||||
@@ -235,17 +233,11 @@ const FavoriteChannelResolvedItem = observer(
|
||||
showFadedUnreadOnMutedChannels: Accessibility.showFadedUnreadOnMutedChannels,
|
||||
unreadBadgesLevel,
|
||||
});
|
||||
const {scheduleChannelPreload, cancelChannelPreload, preloadChannelNow} = useChannelHoverPreload({
|
||||
channel,
|
||||
guild,
|
||||
defaultHiddenForChannel: channel.type === ChannelTypes.GUILD_VOICE,
|
||||
});
|
||||
const handleClick = () => {
|
||||
if (LinkChannelCommands.openLinkChannel(channel)) {
|
||||
return;
|
||||
}
|
||||
markDirectSelection(DirectSelectionSurface.FAVORITES_LIST);
|
||||
preloadChannelNow();
|
||||
NavigationCommands.selectChannel(FAVORITES_GUILD_ID, favoriteChannel.channelId);
|
||||
};
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
@@ -309,8 +301,6 @@ const FavoriteChannelResolvedItem = observer(
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onLongPress={() => {}}
|
||||
onMouseEnter={scheduleChannelPreload}
|
||||
onMouseLeave={cancelChannelPreload}
|
||||
data-flx="app.favorites-channel-list-content.favorite-channel-item.generic-channel-item.click"
|
||||
>
|
||||
<div
|
||||
|
||||
+9
-9
@@ -1,13 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {DirectSelectionSurface, markDirectSelection} from '@app/features/app/components/layout/DirectSelectionOrigin';
|
||||
import {useGuildListItemPreload} from '@app/features/app/components/layout/sidebar_nav/UseGuildListItemPreload';
|
||||
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
|
||||
import {useHover} from '@app/features/app/hooks/useHover';
|
||||
import type {Guild} from '@app/features/guild/models/Guild';
|
||||
import GuildCount from '@app/features/guild/state/GuildCount';
|
||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
|
||||
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
|
||||
import {GuildContextMenu} from '@app/features/ui/action_menu/GuildContextMenu';
|
||||
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
|
||||
import * as AvatarSourceUtils from '@app/features/user/utils/AvatarSourceUtils';
|
||||
@@ -64,12 +65,12 @@ export function useGuildListItemInteraction({
|
||||
}: UseGuildListItemInteractionOptions): GuildListItemInteraction {
|
||||
const [hoverRef, isHovering] = useHover();
|
||||
const contextMenuOpen = useContextMenuHoverState(itemRef, isDesktopLayout);
|
||||
const {preloadChannelNow, selectedChannelId} = useGuildListItemPreload({
|
||||
guild,
|
||||
isHovering,
|
||||
isMobileExperience,
|
||||
isSortingList,
|
||||
});
|
||||
const selectedChannelId = SelectedChannel.selectedChannelIds.get(guild.id) ?? null;
|
||||
useEffect(() => {
|
||||
if (isMobileExperience || isSortingList || !isHovering) return;
|
||||
const timeoutId = window.setTimeout(() => GuildCount.requestCounts(guild.id, {force: false}), 250);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [guild.id, isHovering, isMobileExperience, isSortingList]);
|
||||
const iconURL = AvatarSourceUtils.getGuildIconURL(guild, false);
|
||||
const hoverIconURL = AvatarSourceUtils.getGuildIconURL(guild, true);
|
||||
const isAnimatableIcon = hoverIconURL !== iconURL;
|
||||
@@ -84,13 +85,12 @@ export function useGuildListItemInteraction({
|
||||
}, [contextMenuOpen, hoverIconURL, isAnimatableIcon, isHovering, loadedAnimatedURL]);
|
||||
const handleSelect = useCallback(() => {
|
||||
markDirectSelection(DirectSelectionSurface.GUILD_RAIL);
|
||||
preloadChannelNow();
|
||||
if (isMobileExperience || selectedChannelId == null) {
|
||||
NavigationCommands.selectGuild(guild.id);
|
||||
return;
|
||||
}
|
||||
NavigationCommands.selectGuild(guild.id, selectedChannelId);
|
||||
}, [guild.id, isMobileExperience, preloadChannelNow, selectedChannelId]);
|
||||
}, [guild.id, isMobileExperience, selectedChannelId]);
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (!isKeyboardActivationKey(event.key)) return;
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {useChannelHoverPreload} from '@app/features/app/hooks/useChannelHoverPreload';
|
||||
import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import type {Guild} from '@app/features/guild/models/Guild';
|
||||
import GuildCount from '@app/features/guild/state/GuildCount';
|
||||
import {filterViewableChannels} from '@app/features/messaging/utils/ChannelShared';
|
||||
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {useEffect, useMemo} from 'react';
|
||||
|
||||
interface UseGuildListItemPreloadOptions {
|
||||
readonly guild: Guild;
|
||||
readonly isHovering: boolean;
|
||||
readonly isMobileExperience: boolean;
|
||||
readonly isSortingList: boolean;
|
||||
}
|
||||
|
||||
interface UseGuildListItemPreloadResult {
|
||||
readonly preloadChannelNow: () => void;
|
||||
readonly selectedChannelId: string | null;
|
||||
}
|
||||
|
||||
function resolveSelectedChannel(selectedChannelId: string | null): Channel | null {
|
||||
if (selectedChannelId == null) {
|
||||
return null;
|
||||
}
|
||||
return Channels.getChannel(selectedChannelId) ?? null;
|
||||
}
|
||||
|
||||
function resolveDefaultHiddenForChannel(channel: Channel | null): boolean {
|
||||
if (channel == null) {
|
||||
return false;
|
||||
}
|
||||
return channel.type === ChannelTypes.GUILD_VOICE;
|
||||
}
|
||||
|
||||
export function useGuildListItemPreload({
|
||||
guild,
|
||||
isHovering,
|
||||
isMobileExperience,
|
||||
isSortingList,
|
||||
}: UseGuildListItemPreloadOptions): UseGuildListItemPreloadResult {
|
||||
const storedChannelId = SelectedChannel.selectedChannelIds.get(guild.id);
|
||||
let selectedChannelId: string | null = null;
|
||||
if (storedChannelId != null) {
|
||||
selectedChannelId = storedChannelId;
|
||||
}
|
||||
const selectedChannel = resolveSelectedChannel(selectedChannelId);
|
||||
const guildChannels = Channels.getGuildChannels(guild.id);
|
||||
const preloadTargetChannel = useMemo(() => {
|
||||
if (
|
||||
selectedChannel != null &&
|
||||
selectedChannel.guildId === guild.id &&
|
||||
selectedChannel.type !== ChannelTypes.GUILD_CATEGORY &&
|
||||
selectedChannel.type !== ChannelTypes.GUILD_LINK
|
||||
) {
|
||||
return selectedChannel;
|
||||
}
|
||||
const firstTextChannel = filterViewableChannels(guildChannels)[0];
|
||||
if (firstTextChannel == null) {
|
||||
return null;
|
||||
}
|
||||
return firstTextChannel;
|
||||
}, [guild.id, guildChannels, selectedChannel]);
|
||||
const {scheduleChannelPreload, cancelChannelPreload, preloadChannelNow} = useChannelHoverPreload({
|
||||
channel: preloadTargetChannel,
|
||||
guild,
|
||||
defaultHiddenForChannel: resolveDefaultHiddenForChannel(preloadTargetChannel),
|
||||
enabled: !guild.unavailable && !isSortingList,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (isMobileExperience || isSortingList || !isHovering) {
|
||||
cancelChannelPreload();
|
||||
return;
|
||||
}
|
||||
scheduleChannelPreload();
|
||||
return cancelChannelPreload;
|
||||
}, [cancelChannelPreload, isHovering, isMobileExperience, isSortingList, scheduleChannelPreload]);
|
||||
useEffect(() => {
|
||||
if (isMobileExperience || isSortingList || !isHovering) return;
|
||||
const timeoutId = window.setTimeout(() => GuildCount.requestCounts(guild.id, {force: false}), 250);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [guild.id, isHovering, isMobileExperience, isSortingList]);
|
||||
return {preloadChannelNow, selectedChannelId};
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import type {Guild} from '@app/features/guild/models/Guild';
|
||||
import {ensureMembersForMessages} from '@app/features/messaging/commands/MessageCommands';
|
||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
|
||||
const CHANNEL_HOVER_PRELOAD_DELAY_MS = 220;
|
||||
|
||||
interface UseChannelHoverPreloadOptions {
|
||||
channel: Channel | null | undefined;
|
||||
guild?: Guild | null;
|
||||
defaultHiddenForChannel?: boolean;
|
||||
enabled?: boolean;
|
||||
preloadMemberList?: boolean;
|
||||
preloadMessages?: boolean;
|
||||
}
|
||||
|
||||
export function ensureMembersForCachedChannelMessages(channelId: string): void {
|
||||
const messages = Messages.getCachedMessages(channelId);
|
||||
if (!messages || messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
void ensureMembersForMessages(messages.toArray());
|
||||
}
|
||||
|
||||
export function useChannelHoverPreload({
|
||||
channel,
|
||||
guild = null,
|
||||
enabled = true,
|
||||
preloadMessages = true,
|
||||
}: UseChannelHoverPreloadOptions): {
|
||||
scheduleChannelPreload: () => void;
|
||||
cancelChannelPreload: () => void;
|
||||
preloadChannelNow: () => void;
|
||||
} {
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const cancelChannelPreload = useCallback(() => {
|
||||
if (timerRef.current == null || typeof window === 'undefined') {
|
||||
timerRef.current = null;
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}, []);
|
||||
const preloadChannelNow = useCallback(() => {
|
||||
cancelChannelPreload();
|
||||
if (
|
||||
!enabled ||
|
||||
!channel ||
|
||||
channel.type === ChannelTypes.GUILD_CATEGORY ||
|
||||
channel.type === ChannelTypes.GUILD_LINK
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (preloadMessages) {
|
||||
if (!Messages.preloadLatestPage(channel.id, guild?.id ?? channel.guildId ?? null)) {
|
||||
ensureMembersForCachedChannelMessages(channel.id);
|
||||
}
|
||||
}
|
||||
}, [cancelChannelPreload, channel, enabled, guild, preloadMessages]);
|
||||
const scheduleChannelPreload = useCallback(() => {
|
||||
if (!enabled || !channel || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
cancelChannelPreload();
|
||||
timerRef.current = window.setTimeout(preloadChannelNow, CHANNEL_HOVER_PRELOAD_DELAY_MS);
|
||||
}, [cancelChannelPreload, channel, enabled, preloadChannelNow]);
|
||||
useEffect(() => cancelChannelPreload, [cancelChannelPreload]);
|
||||
return {scheduleChannelPreload, cancelChannelPreload, preloadChannelNow};
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {LongPressable} from '@app/features/app/components/LongPressable';
|
||||
import {getChannelUnreadState} from '@app/features/app/components/layout/utils/ChannelUnreadState';
|
||||
import {CustomStatusDisplay} from '@app/features/app/components/shared/custom_status_display/CustomStatusDisplay';
|
||||
import {GroupDMAvatar} from '@app/features/app/components/shared/GroupDMAvatar';
|
||||
import {useChannelHoverPreload} from '@app/features/app/hooks/useChannelHoverPreload';
|
||||
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
|
||||
import {UserTag} from '@app/features/channel/components/ChannelUserTag';
|
||||
import styles from '@app/features/channel/components/direct_message/DirectMessageList.module.css';
|
||||
@@ -199,15 +198,9 @@ const ResolvedDMListItem = observer(function ResolvedDMListItem({
|
||||
leaveGroup,
|
||||
i18n,
|
||||
});
|
||||
const {scheduleChannelPreload, cancelChannelPreload, preloadChannelNow} = useChannelHoverPreload({
|
||||
channel,
|
||||
guild: null,
|
||||
preloadMemberList: false,
|
||||
});
|
||||
const handleNavigate = useCallback(() => {
|
||||
preloadChannelNow();
|
||||
handlers.navigateTo();
|
||||
}, [handlers.navigateTo, preloadChannelNow]);
|
||||
}, [handlers.navigateTo]);
|
||||
const mobileMenuGroups = buildMobileMenuGroups({
|
||||
channel,
|
||||
recipient,
|
||||
@@ -507,8 +500,6 @@ const ResolvedDMListItem = observer(function ResolvedDMListItem({
|
||||
)}
|
||||
onClick={handleNavigate}
|
||||
onContextMenu={handleContextMenu}
|
||||
onMouseEnter={scheduleChannelPreload}
|
||||
onMouseLeave={cancelChannelPreload}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
data-dm-list-focus-item="true"
|
||||
|
||||
@@ -206,26 +206,6 @@ class Messages {
|
||||
return messages.ready && messages.length > 0 && !messages.cached;
|
||||
}
|
||||
|
||||
shouldPreloadLatestPage(channelId: string): boolean {
|
||||
if (!GatewayConnection.isConnected || !Channels.getChannel(channelId)) {
|
||||
return false;
|
||||
}
|
||||
const messages = ChannelMessages.get(channelId);
|
||||
if (!messages) return true;
|
||||
if (messages.loadingMore || ChannelMessages.isRetained(channelId)) return false;
|
||||
return messages.length === 0 ? !messages.ready : messages.cached;
|
||||
}
|
||||
|
||||
preloadLatestPage(channelId: string, guildId?: string | null): boolean {
|
||||
if (!this.shouldPreloadLatestPage(channelId)) {
|
||||
return false;
|
||||
}
|
||||
const channel = Channels.getChannel(channelId);
|
||||
const resolvedGuildId = guildId ?? channel?.guildId ?? (channel?.isPrivate() ? ME : undefined);
|
||||
this.handleChannelSelect({guildId: resolvedGuildId ?? undefined, channelId});
|
||||
return true;
|
||||
}
|
||||
|
||||
getMessage(channelId: string, messageId: string): Message | undefined {
|
||||
return ChannelMessages.getOrCreate(channelId).get(messageId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user