diff --git a/fluxer_app/src/features/channel/components/ChannelMembers.tsx b/fluxer_app/src/features/channel/components/ChannelMembers.tsx index b8d60b55c..1f8ea2da8 100644 --- a/fluxer_app/src/features/channel/components/ChannelMembers.tsx +++ b/fluxer_app/src/features/channel/components/ChannelMembers.tsx @@ -18,6 +18,8 @@ import { MEMBER_LIST_GROUP_HEADER_HEIGHT_PX, MEMBER_LIST_ITEM_HEIGHT_PX, MEMBER_LIST_METRICS_STYLE, + MEMBER_LIST_SCROLL_CHUNK_PX, + MEMBER_LIST_SUBSCRIPTION_VIEWPORT_RATIO, } from '@app/features/channel/components/MemberListMetrics'; import { MemberListSkeleton, @@ -28,8 +30,6 @@ import {MemberListUnavailableFallback} from '@app/features/channel/components/sh import type {Channel} from '@app/features/channel/models/Channel'; import type {Guild} from '@app/features/guild/models/Guild'; import {OFFLINE_DESCRIPTOR, ONLINE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; -import {resolveMemberListCustomStatus} from '@app/features/member/hooks/useMemberListCustomStatus'; -import {resolveMemberListPresence} from '@app/features/member/hooks/useMemberListPresence'; import {useMemberListSubscription} from '@app/features/member/hooks/useMemberListSubscription'; import {resolveMemberListViewportModel} from '@app/features/member/state/MemberListViewportStateMachine'; import MemberSidebar from '@app/features/member/state/MemberSidebar'; @@ -38,6 +38,7 @@ import { buildMemberListRowOffsets, getGroupLayoutForRow, getTotalRowsFromLayout, + quantizeMemberListScrollSpan, } from '@app/features/member/utils/MemberListLayout'; import { areNormalizedMemberListRangesEqual, @@ -60,6 +61,7 @@ import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {MEMBER_LIST_RANGE_MAX_SPAN} from '@fluxer/constants/src/GatewayConstants'; import {GuildOperations} from '@fluxer/constants/src/GuildConstants'; +import {MEDIA_PROXY_AVATAR_SIZE_DEFAULT} from '@fluxer/constants/src/MediaProxyAssetSizes'; import {useLingui as useLinguiRuntime} from '@lingui/react'; import {useLingui} from '@lingui/react/macro'; import clsx from 'clsx'; @@ -72,11 +74,10 @@ const INITIAL_RENDER_RANGE: [number, number] = [0, 64]; const INITIAL_SUBSCRIPTION_RANGES = normalizeMemberListRanges([INITIAL_SUBSCRIPTION_RANGE]); const INITIAL_RENDER_RANGES = normalizeMemberListRanges([INITIAL_RENDER_RANGE]); const EMPTY_MEMBER_LIST_RANGES = normalizeMemberListRanges([]); -const SUBSCRIPTION_BUFFER_ROWS = 12; const SUBSCRIPTION_OVERSCAN_PAGES = 0; -const RENDER_BUFFER_ROWS = 6; -const AVATAR_DEFER_AFTER_SCROLL_IDLE_MS = 180; -const MEMBER_LIST_AVATAR_MEDIA_SIZE = 64; +const MIN_SUBSCRIPTION_BUFFER_ROWS = 1; +const RESIDENT_SUBSCRIPTION_RANGE: [number, number] = [0, MEMBER_LIST_RANGE_MAX_SPAN]; +const EMPTY_MEMBER_GROUPS: NonNullable>['groups'] = []; function measureMemberGroupHeadingWidthPx(element: HTMLElement | null): number { if (element == null) { @@ -95,36 +96,41 @@ function measureMemberGroupHeadingWidthPx(element: HTMLElement | null): number { return Math.round((right - left) / getRemScaleForDocument(element.ownerDocument)); } -function createGroupHeadingRegistrar(groupHeadingWidths: Map): (node: HTMLDivElement | null) => void { - return (node) => { - if (node == null) { - return; - } - const groupId = node.dataset.memberGroupId; - if (groupId == null) { - return; - } - const widthPx = measureMemberGroupHeadingWidthPx(node); - if (widthPx !== SKELETON_UNMEASURED_WIDTH_PX) { - groupHeadingWidths.set(groupId, widthPx); - } - }; -} - interface GroupHeadingWidthTracking { - contentKey: string; groupHeadingWidths: Map; registerGroupHeading: (node: HTMLDivElement | null) => void; + captureGroupHeadingWidths: () => void; } -function useGroupHeadingWidthTracking(contentKey: string): GroupHeadingWidthTracking { +function useGroupHeadingWidthTracking(): GroupHeadingWidthTracking { const trackingRef = useRef(null); - if (trackingRef.current == null || trackingRef.current.contentKey !== contentKey) { - const groupHeadingWidths = trackingRef.current?.groupHeadingWidths ?? new Map(); + if (trackingRef.current == null) { + const groupHeadingWidths = new Map(); + const groupHeadingNodes = new Map(); trackingRef.current = { - contentKey, groupHeadingWidths, - registerGroupHeading: createGroupHeadingRegistrar(groupHeadingWidths), + registerGroupHeading: (node) => { + if (node == null) { + return; + } + const groupId = node.dataset.memberGroupId; + if (groupId == null) { + return; + } + groupHeadingNodes.set(groupId, node); + }, + captureGroupHeadingWidths: () => { + for (const [groupId, node] of groupHeadingNodes) { + if (!node.isConnected) { + groupHeadingNodes.delete(groupId); + continue; + } + const widthPx = measureMemberGroupHeadingWidthPx(node); + if (widthPx !== SKELETON_UNMEASURED_WIDTH_PX) { + groupHeadingWidths.set(groupId, widthPx); + } + } + }, }; } return trackingRef.current; @@ -211,12 +217,10 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe const subscriptionRangesRef = useRef(INITIAL_SUBSCRIPTION_RANGES); const renderRangesRef = useRef(INITIAL_RENDER_RANGES); const scrollFrameRef = useRef(null); - const avatarDeferTimerRef = useRef(null); - const avatarDeferDeadlineRef = useRef(0); const pendingScrollMetricsRef = useRef<{scrollTop: number; clientHeight: number} | null>(null); const scrollerRef = useRef(null); + const viewportHeightRef = useRef(null); const [renderWindowRanges, setRenderWindowRanges] = useState(INITIAL_RENDER_RANGES); - const [deferAvatarLoad, setDeferAvatarLoad] = useState(false); const memberListIdentityKey = MemberSidebar.getListIdentityKey(guild.id, channel.id); const memberListUpdatesDisabled = (guild.disabledOperations & GuildOperations.MEMBER_LIST_UPDATES) !== 0; const currentUserId = Authentication.currentUserId; @@ -229,10 +233,10 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe }); const memberListState = MemberSidebar.getList(guild.id, channel.id); const memberCount = memberListState?.memberCount ?? 0; - const groups = memberListState?.groups ?? []; + const groups = memberListState?.groups ?? EMPTY_MEMBER_GROUPS; const populatedGroups = useMemo(() => groups.filter((group) => group.count > 0), [groups]); const memberGroupCountsKey = populatedGroups.map((group) => `${group.id}:${group.count}`).join(','); - const {groupHeadingWidths, registerGroupHeading} = useGroupHeadingWidthTracking(memberGroupCountsKey); + const {groupHeadingWidths, registerGroupHeading, captureGroupHeadingWidths} = useGroupHeadingWidthTracking(); const memberSurfaceKind = channel.type === ChannelTypes.GUILD_VOICE ? SkeletonMemberSurfaceKind.GUILD_VOICE : SkeletonMemberSurfaceKind.GUILD; const rememberedMemberGroups = getRememberedSkeletonMemberGroups(channel.id, memberSurfaceKind); @@ -248,6 +252,7 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe const remScale = getAppRemScale(); const scaledMemberItemHeight = MEMBER_LIST_ITEM_HEIGHT_PX * remScale; const scaledGroupHeaderHeight = MEMBER_LIST_GROUP_HEADER_HEIGHT_PX * remScale; + const scaledScrollChunk = MEMBER_LIST_SCROLL_CHUNK_PX * remScale; const rowOffsets = useMemo( () => layouts.length > 0 @@ -280,6 +285,7 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe if (memberListUpdatesDisabled || lacksMemberViewPermission || !memberListState?.hasReceivedInitialPayload) { return; } + captureGroupHeadingWidths(); reportSkeletonMemberLayout( channel.id, memberSurfaceKind, @@ -305,21 +311,29 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe ); const commitRangeUpdate = useCallback( (scrollTop: number, clientHeight: number) => { - const nextSubscriptionRanges = buildMemberListRangeWindow({ - scrollTop, - clientHeight, - rowHeight: scaledMemberItemHeight, - rowOffsets, - bufferRows: SUBSCRIPTION_BUFFER_ROWS, - overscanPages: SUBSCRIPTION_OVERSCAN_PAGES, - totalRows: totalRows > 0 ? totalRows : undefined, - }); + const subscriptionBufferRows = Math.max( + MIN_SUBSCRIPTION_BUFFER_ROWS, + Math.ceil((clientHeight * MEMBER_LIST_SUBSCRIPTION_VIEWPORT_RATIO) / scaledMemberItemHeight), + ); + const nextSubscriptionRanges = normalizeMemberListRanges([ + RESIDENT_SUBSCRIPTION_RANGE, + ...buildMemberListRangeWindow({ + scrollTop, + clientHeight, + rowHeight: scaledMemberItemHeight, + rowOffsets, + bufferRows: subscriptionBufferRows, + overscanPages: SUBSCRIPTION_OVERSCAN_PAGES, + totalRows: totalRows > 0 ? totalRows : undefined, + }), + ]); + const renderSpan = quantizeMemberListScrollSpan(scrollTop, clientHeight, scaledScrollChunk); const nextRenderRanges = buildMemberListRenderWindow({ - scrollTop, - clientHeight, + scrollTop: renderSpan.top, + clientHeight: renderSpan.height, rowHeight: scaledMemberItemHeight, rowOffsets, - bufferRows: RENDER_BUFFER_ROWS, + bufferRows: 0, totalRows: totalRows > 0 ? totalRows : undefined, }); if (!areNormalizedMemberListRangesEqual(nextRenderRanges, renderRangesRef.current)) { @@ -331,29 +345,8 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe subscribe(nextSubscriptionRanges); } }, - [subscribe, totalRows, rowOffsets, scaledMemberItemHeight, zoomLevel], + [subscribe, totalRows, rowOffsets, scaledMemberItemHeight, scaledScrollChunk, zoomLevel], ); - const finishAvatarLoadingDeferralAfterIdle = useCallback(() => { - avatarDeferTimerRef.current = null; - const remainingMs = avatarDeferDeadlineRef.current - performance.now(); - if (remainingMs > 0) { - avatarDeferTimerRef.current = window.setTimeout(finishAvatarLoadingDeferralAfterIdle, remainingMs); - return; - } - setDeferAvatarLoad(false); - }, []); - const markAvatarLoadingDeferred = useCallback(() => { - if (!deferAvatarLoad) { - setDeferAvatarLoad(true); - } - avatarDeferDeadlineRef.current = performance.now() + AVATAR_DEFER_AFTER_SCROLL_IDLE_MS; - if (avatarDeferTimerRef.current == null) { - avatarDeferTimerRef.current = window.setTimeout( - finishAvatarLoadingDeferralAfterIdle, - AVATAR_DEFER_AFTER_SCROLL_IDLE_MS, - ); - } - }, [deferAvatarLoad, finishAvatarLoadingDeferralAfterIdle]); const flushScrollRangeUpdate = useCallback(() => { scrollFrameRef.current = null; const metrics = pendingScrollMetricsRef.current; @@ -383,20 +376,25 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe const handleScroll = useCallback( (event: UIEvent) => { const target = event.currentTarget; - markAvatarLoadingDeferred(); - scheduleRangeUpdate(target.scrollTop, target.clientHeight); + let viewportHeight = viewportHeightRef.current; + if (viewportHeight == null) { + viewportHeight = target.clientHeight; + viewportHeightRef.current = viewportHeight; + } + scheduleRangeUpdate(target.scrollTop, viewportHeight); }, - [markAvatarLoadingDeferred, scheduleRangeUpdate], + [scheduleRangeUpdate], ); const handleResize = useCallback(() => { + viewportHeightRef.current = null; scheduleRangeUpdateFromScroller(); }, [scheduleRangeUpdateFromScroller]); useEffect(() => { - setDeferAvatarLoad(false); const initialSubscriptionRanges = INITIAL_SUBSCRIPTION_RANGES; const initialRenderRanges = INITIAL_RENDER_RANGES; subscriptionRangesRef.current = initialSubscriptionRanges; renderRangesRef.current = initialRenderRanges; + viewportHeightRef.current = null; setRenderWindowRanges(initialRenderRanges); }, [memberListIdentityKey, guild.id]); useEffect(() => { @@ -408,11 +406,6 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe window.cancelAnimationFrame(scrollFrameRef.current); scrollFrameRef.current = null; } - if (avatarDeferTimerRef.current != null) { - window.clearTimeout(avatarDeferTimerRef.current); - avatarDeferTimerRef.current = null; - } - avatarDeferDeadlineRef.current = 0; pendingScrollMetricsRef.current = null; }; }, [memberListIdentityKey, guild.id]); @@ -457,6 +450,7 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe ); } const virtualRows: Array = []; + const renderedMemberIds = new Set(); for (const [rangeStart, rangeEnd] of renderRanges) { const firstRow = Math.max(0, rangeStart); const lastRow = totalRows > 0 ? Math.min(rangeEnd, totalRows - 1) : -1; @@ -487,19 +481,17 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe continue; } const user = member.user; + if (renderedMemberIds.has(user.id)) { + continue; + } + renderedMemberIds.add(user.id); const displayName = member.nick ? NicknameUtils.formatNicknameForStreamerMode(member.nick) : NicknameUtils.getNickname(user, guild.id); - const status = resolveMemberListPresence({guildId: guild.id, channelId: channel.id, userId: user.id}); - const customStatus = resolveMemberListCustomStatus({ - guildId: guild.id, - channelId: channel.id, - userId: user.id, - }); const roleColor = member.getColorString?.() ?? undefined; virtualRows.push(
, @@ -542,7 +530,7 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe if (role) { virtualRows.push(
, @@ -672,8 +654,9 @@ const GroupDMChannelMembers = observer(function GroupDMChannelMembers({channel}: const memberGroupContentKey = memberGroups .map((group) => `${group.id}:${group.displayName}:${group.count}`) .join(','); - const {groupHeadingWidths, registerGroupHeading} = useGroupHeadingWidthTracking(memberGroupContentKey); + const {groupHeadingWidths, registerGroupHeading, captureGroupHeadingWidths} = useGroupHeadingWidthTracking(); useSkeletonLayoutReport(() => { + captureGroupHeadingWidths(); reportSkeletonMemberLayout( channel.id, SkeletonMemberSurfaceKind.GROUP_DM, diff --git a/fluxer_app/src/features/channel/components/CompactMemberCustomStatus.tsx b/fluxer_app/src/features/channel/components/CompactMemberCustomStatus.tsx index cb56c8031..17ce0b3e1 100644 --- a/fluxer_app/src/features/channel/components/CompactMemberCustomStatus.tsx +++ b/fluxer_app/src/features/channel/components/CompactMemberCustomStatus.tsx @@ -1,13 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import sharedStyles from '@app/features/app/components/shared/custom_status_display/CustomStatusDisplay.module.css'; +import {buildCustomEmojiURL} from '@app/features/expressions/utils/CustomEmojiImageUrl'; import {getEmojiURL as getUnicodeEmojiURL} from '@app/features/expressions/utils/EmojiUtils'; import {usePresenceCustomStatus} from '@app/features/presence/hooks/usePresenceCustomStatus'; import {useTextOverflow} from '@app/features/ui/hooks/useTextOverflow'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import type {CustomStatus} from '@app/features/user/state/CustomStatus'; import {getCustomStatusText, isCustomStatusExpired, normalizeCustomStatus} from '@app/features/user/state/CustomStatus'; -import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; import clsx from 'clsx'; import {type ReactNode, useEffect, useMemo, useRef, useState} from 'react'; @@ -16,7 +16,6 @@ interface CompactMemberCustomStatusProps { customStatus?: CustomStatus | null; userId?: string; showText?: boolean; - deferMediaLoad?: boolean; } function sanitizeText(text: string): string { @@ -45,33 +44,14 @@ export function hasVisibleCompactMemberCustomStatus(status: CustomStatus | null return sanitizeText(normalized.text ?? '').length > 0; } -const loadedStatusMediaUrls = new Set(); - -function markStatusMediaLoaded(url: string): void { - loadedStatusMediaUrls.add(url); -} - -function renderEmoji(status: CustomStatus, deferMediaLoad: boolean): ReactNode { +function renderEmoji(status: CustomStatus): ReactNode { if (status.emojiId) { - const emojiUrl = AvatarUtils.getEmojiURL({id: status.emojiId, animated: false}); - if (deferMediaLoad && !loadedStatusMediaUrls.has(emojiUrl)) { - return ( - - ); - } return ( {status.emojiName markStatusMediaLoaded(emojiUrl)} data-flx="channel.compact-member-custom-status.emoji" /> ); @@ -83,24 +63,12 @@ function renderEmoji(status: CustomStatus, deferMediaLoad: boolean): ReactNode { if (!twemojiUrl) { return null; } - if (deferMediaLoad && !loadedStatusMediaUrls.has(twemojiUrl)) { - return ( - - ); - } return ( {status.emojiName} markStatusMediaLoaded(twemojiUrl)} data-flx="channel.compact-member-custom-status.unicode-emoji" /> ); @@ -111,7 +79,6 @@ export function CompactMemberCustomStatus({ customStatus, userId, showText = true, - deferMediaLoad = false, }: CompactMemberCustomStatusProps) { const containerRef = useRef(null); const shouldFetchFromPresence = customStatus === undefined && userId !== undefined; @@ -143,7 +110,7 @@ export function CompactMemberCustomStatus({ return null; } const text = normalized.text ? sanitizeText(normalized.text) : null; - const emoji = renderEmoji(normalized, deferMediaLoad); + const emoji = renderEmoji(normalized); if (!emoji && (!showText || !text)) { return null; } diff --git a/fluxer_app/src/features/channel/components/MemberListItem.tsx b/fluxer_app/src/features/channel/components/MemberListItem.tsx index b3a259111..14b45f8f3 100644 --- a/fluxer_app/src/features/channel/components/MemberListItem.tsx +++ b/fluxer_app/src/features/channel/components/MemberListItem.tsx @@ -16,7 +16,6 @@ import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuComma import {FocusRingWrapper} from '@app/features/ui/components/FocusRingWrapper'; import {ListStatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar'; import {useTextOverflow} from '@app/features/ui/hooks/useTextOverflow'; -import ContextMenu from '@app/features/ui/state/ContextMenu'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import type {User} from '@app/features/user/models/User'; import type {CustomStatus} from '@app/features/user/state/CustomStatus'; @@ -30,10 +29,9 @@ import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {CrownIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; -import {autorun} from 'mobx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useCallback, useMemo, useRef, useState} from 'react'; const COMMUNITY_OWNER_DESCRIPTOR = msg({ message: 'Community owner', @@ -57,8 +55,6 @@ interface MemberListItemProps { roleColor?: string; displayName?: string; disableBackdrop?: boolean; - deferAvatarLoad?: boolean; - deferCustomStatusMedia?: boolean; avatarMediaSize?: MediaProxyImageSize; } @@ -75,7 +71,6 @@ export const MemberListItem: React.FC = observer((props) => roleColor, displayName, disableBackdrop = false, - deferCustomStatusMedia = false, avatarMediaSize, } = props; const itemRef = useRef(null); @@ -83,9 +78,9 @@ export const MemberListItem: React.FC = observer((props) => guildId: guildId ?? '', channelId, userId: user.id, - enabled: guildId !== undefined && providedStatus === undefined, + enabled: providedStatus === undefined, }); - const status = providedStatus ?? hookStatus; + const status = providedStatus !== undefined ? providedStatus : hookStatus; const hookCustomStatus = useMemberListCustomStatus({ guildId: guildId ?? '', channelId, @@ -94,53 +89,53 @@ export const MemberListItem: React.FC = observer((props) => }); const memberListCustomStatus = providedCustomStatus !== undefined ? providedCustomStatus : hookCustomStatus; const [contextMenuOpen, setContextMenuOpen] = useState(false); + const contextMenuTicketRef = useRef(0); const isCurrentUser = user.id === Authentication.currentUserId; const isTyping = TypingIndicator.isMemberListTyping(channelId, user.id, Authentication.currentUserId); - useEffect(() => { - const disposer = autorun(() => { - const contextMenu = ContextMenu.contextMenu; - const targetElement = contextMenu?.target.target; - const isNodeTarget = typeof Node !== 'undefined' && targetElement instanceof Node; - const isOpen = Boolean(contextMenu && isNodeTarget && itemRef.current?.contains(targetElement)); - setContextMenuOpen(isOpen); - }); - return () => { - disposer(); - }; - }, []); const handleContextMenu = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); - ContextMenuCommands.openFromEvent(event, ({onClose}) => ( - <> - {guildId ? ( - - ) : ( - - )} - - )); + const ticket = contextMenuTicketRef.current + 1; + contextMenuTicketRef.current = ticket; + setContextMenuOpen(true); + ContextMenuCommands.openFromEvent( + event, + ({onClose}) => ( + <> + {guildId ? ( + + ) : ( + + )} + + ), + { + onClose: () => { + if (contextMenuTicketRef.current === ticket) { + setContextMenuOpen(false); + } + }, + }, + ); }, [user, guildId, channelId, guildMember], ); - const ownerTitle = guildId ? i18n._(COMMUNITY_OWNER_DESCRIPTOR) : i18n._(GROUP_OWNER_DESCRIPTOR); - const hideOwnerCrown = guildId - ? (Guilds.getGuild(guildId)?.features.has(GuildFeatures.HIDE_OWNER_CROWN) ?? false) - : false; - const showOwnerCrown = isOwner && !hideOwnerCrown; + const showOwnerCrown = + isOwner && + !(guildId !== undefined && (Guilds.getGuild(guildId)?.features.has(GuildFeatures.HIDE_OWNER_CROWN) ?? false)); const nickname = displayName || NicknameUtils.getNickname(user, guildId, channelId); const nameRef = useRef(null); const isNameOverflowing = useTextOverflow(nameRef, {content: nickname, measureTextRange: true}); @@ -213,7 +208,7 @@ export const MemberListItem: React.FC = observer((props) => avatarUrl={memberAvatarUrl} hoverAvatarUrl={memberHoverAvatarUrl} mediaSize={avatarMediaSize} - data-flx="channel.member-list-item.status-aware-avatar" + data-flx="channel.member-list-item.status-aware-avatar" />
@@ -227,7 +222,10 @@ export const MemberListItem: React.FC = observer((props) => )} {showOwnerCrown && (
- +
@@ -244,7 +242,6 @@ export const MemberListItem: React.FC = observer((props) => customStatus={memberListCustomStatus} userId={user.id} className={styles.memberCustomStatus} - deferMediaLoad={deferCustomStatusMedia} data-flx="channel.member-list-item.member-custom-status" />
@@ -261,7 +258,6 @@ export const MemberListItem: React.FC = observer((props) => guildId={guildId} guildMember={guildMember} channelId={channelId} - key={user.id} disableContextMenu disableBackdrop={disableBackdrop} profilePopoutAnimationType="profile-slide-inverted" diff --git a/fluxer_app/src/features/channel/components/MemberListMetrics.ts b/fluxer_app/src/features/channel/components/MemberListMetrics.ts index a18097fce..33b5e4cec 100644 --- a/fluxer_app/src/features/channel/components/MemberListMetrics.ts +++ b/fluxer_app/src/features/channel/components/MemberListMetrics.ts @@ -7,6 +7,8 @@ import type {CSSProperties} from 'react'; export const MEMBER_LIST_ITEM_HEIGHT_PX = 44; export const MEMBER_LIST_GROUP_DM_ITEM_HEIGHT_PX = 40; export const MEMBER_LIST_GROUP_HEADER_HEIGHT_PX = 32; +export const MEMBER_LIST_SCROLL_CHUNK_PX = 256; +export const MEMBER_LIST_SUBSCRIPTION_VIEWPORT_RATIO = 0.5; const MEMBER_LIST_METRICS = { '--member-list-item-height': remFromPx(MEMBER_LIST_ITEM_HEIGHT_PX), diff --git a/fluxer_app/src/features/channel/components/bottomsheets/ChannelDetailsBottomSheetMemberList.tsx b/fluxer_app/src/features/channel/components/bottomsheets/ChannelDetailsBottomSheetMemberList.tsx index 0fa0db6dd..9c0308307 100644 --- a/fluxer_app/src/features/channel/components/bottomsheets/ChannelDetailsBottomSheetMemberList.tsx +++ b/fluxer_app/src/features/channel/components/bottomsheets/ChannelDetailsBottomSheetMemberList.tsx @@ -31,12 +31,13 @@ import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {MEMBER_LIST_RANGE_MAX_SPAN} from '@fluxer/constants/src/GatewayConstants'; import {GuildFeatures, GuildOperations} from '@fluxer/constants/src/GuildConstants'; +import {MEDIA_PROXY_AVATAR_SIZE_DEFAULT} from '@fluxer/constants/src/MediaProxyAssetSizes'; import {isOfflineStatus} from '@fluxer/constants/src/StatusConstants'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import clsx from 'clsx'; import {observer} from 'mobx-react-lite'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; const COMMUNITY_OWNER_DESCRIPTOR = msg({ message: 'Community owner', @@ -46,8 +47,6 @@ const MEMBER_ITEM_HEIGHT = 56; const INITIAL_MEMBER_RANGE: [number, number] = [0, MEMBER_LIST_RANGE_MAX_SPAN]; const SCROLL_BUFFER_ROWS = 10; const SUBSCRIPTION_OVERSCAN_PAGES = 0; -const AVATAR_DEFER_AFTER_SCROLL_IDLE_MS = 180; -const MEMBER_LIST_AVATAR_MEDIA_SIZE = 80; function isScrollableOverflow(value: string): boolean { return value === 'auto' || value === 'scroll' || value === 'overlay'; @@ -109,13 +108,11 @@ export const MobileMemberListItem = observer( guild, channelId, member, - deferAvatarLoad = false, onLongPress, }: { guild: Guild; channelId: string; member: GuildMember; - deferAvatarLoad?: boolean; onLongPress?: (member: GuildMember) => void; }) => { const {i18n} = useLingui(); @@ -140,7 +137,7 @@ export const MobileMemberListItem = observer( memberAvatar: member.avatar, avatarUnset: member.isAvatarUnset(), animated: false, - size: MEMBER_LIST_AVATAR_MEDIA_SIZE, + size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT, }), [guild.id, member], ); @@ -152,7 +149,7 @@ export const MobileMemberListItem = observer( memberAvatar: member.avatar, avatarUnset: member.isAvatarUnset(), animated: true, - size: MEMBER_LIST_AVATAR_MEDIA_SIZE, + size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT, }), [guild.id, member], ); @@ -180,8 +177,8 @@ export const MobileMemberListItem = observer( status={status} avatarUrl={avatarUrl} hoverAvatarUrl={hoverAvatarUrl} - mediaSize={MEMBER_LIST_AVATAR_MEDIA_SIZE} - data-flx="channel.channel-details-bottom-sheet-member-list.mobile-member-list-item.status-aware-avatar" + mediaSize={MEDIA_PROXY_AVATAR_SIZE_DEFAULT} + data-flx="channel.channel-details-bottom-sheet-member-list.mobile-member-list-item.status-aware-avatar" />
)} @@ -256,12 +252,11 @@ interface LazyMemberListGroupProps { group: {id: string; count: number}; channelId: string; members: Array; - deferAvatarLoad?: boolean; onMemberLongPress?: (member: GuildMember) => void; } const LazyMemberListGroup = observer( - ({guild, group, channelId, members, deferAvatarLoad = false, onMemberLongPress}: LazyMemberListGroupProps) => { + ({guild, group, channelId, members, onMemberLongPress}: LazyMemberListGroupProps) => { const {i18n} = useLingui(); const groupName = (() => { switch (group.id) { @@ -296,7 +291,6 @@ const LazyMemberListGroup = observer( guild={guild} channelId={channelId} member={member} - deferAvatarLoad={deferAvatarLoad} onLongPress={onMemberLongPress} data-flx="channel.channel-details-bottom-sheet-member-list.lazy-member-list-group.mobile-member-list-item" /> @@ -328,8 +322,6 @@ const LazyGuildMemberList = observer( const subscribedRangesRef = useRef>([INITIAL_MEMBER_RANGE]); const listContainerRef = useRef(null); const scrollAnimationFrameRef = useRef(null); - const avatarDeferTimerRef = useRef(null); - const [deferAvatarLoad, setDeferAvatarLoad] = useState(false); const memberListUpdatesDisabled = (guild.disabledOperations & GuildOperations.MEMBER_LIST_UPDATES) !== 0; const currentUserId = Authentication.currentUserId; const lacksMemberViewPermission = @@ -366,16 +358,6 @@ const LazyGuildMemberList = observer( }, [subscribe, totalRows], ); - const markAvatarLoadingDeferred = useCallback(() => { - setDeferAvatarLoad(true); - if (avatarDeferTimerRef.current != null) { - window.clearTimeout(avatarDeferTimerRef.current); - } - avatarDeferTimerRef.current = window.setTimeout(() => { - avatarDeferTimerRef.current = null; - setDeferAvatarLoad(false); - }, AVATAR_DEFER_AFTER_SCROLL_IDLE_MS); - }, []); useEffect(() => { if (!enabled || memberListUpdatesDisabled || lacksMemberViewPermission) { return; @@ -393,7 +375,6 @@ const LazyGuildMemberList = observer( updateSubscribedRange(scrollParent.scrollTop, scrollParent.clientHeight); }; const handleScroll = () => { - markAvatarLoadingDeferred(); if (scrollAnimationFrameRef.current !== null) { return; } @@ -417,18 +398,8 @@ const LazyGuildMemberList = observer( window.cancelAnimationFrame(scrollAnimationFrameRef.current); scrollAnimationFrameRef.current = null; } - if (avatarDeferTimerRef.current !== null) { - window.clearTimeout(avatarDeferTimerRef.current); - avatarDeferTimerRef.current = null; - } }; - }, [ - enabled, - memberListUpdatesDisabled, - lacksMemberViewPermission, - updateSubscribedRange, - markAvatarLoadingDeferred, - ]); + }, [enabled, memberListUpdatesDisabled, lacksMemberViewPermission, updateSubscribedRange]); if (lacksMemberViewPermission) { return (