mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
perf(app): window sticker picker rows by offset instead of observers (#1902)
This commit is contained in:
1 parent
b71ced9b2e
commit
aabc13e3cb
4 files changed
+333
-156
No files matched your search
@@ -13,6 +13,12 @@ import {PickerEmptyState} from '@app/features/channel/components/shared/PickerEm
|
|||||||
import {useStickerCategories} from '@app/features/channel/components/sticker_picker/hooks/useStickerCategories';
|
import {useStickerCategories} from '@app/features/channel/components/sticker_picker/hooks/useStickerCategories';
|
||||||
import {useVirtualRows} from '@app/features/channel/components/sticker_picker/hooks/useVirtualRows';
|
import {useVirtualRows} from '@app/features/channel/components/sticker_picker/hooks/useVirtualRows';
|
||||||
import {StickerPickerCategoryList} from '@app/features/channel/components/sticker_picker/StickerPickerCategoryList';
|
import {StickerPickerCategoryList} from '@app/features/channel/components/sticker_picker/StickerPickerCategoryList';
|
||||||
|
import {
|
||||||
|
buildStickerRowOffsets,
|
||||||
|
getStickerRowHeight,
|
||||||
|
getStickerRowWindow,
|
||||||
|
type StickerRowKind,
|
||||||
|
} from '@app/features/channel/components/sticker_picker/StickerPickerConstants';
|
||||||
import {StickerPickerInspector} from '@app/features/channel/components/sticker_picker/StickerPickerInspector';
|
import {StickerPickerInspector} from '@app/features/channel/components/sticker_picker/StickerPickerInspector';
|
||||||
import {StickerPickerSearchBar} from '@app/features/channel/components/sticker_picker/StickerPickerSearchBar';
|
import {StickerPickerSearchBar} from '@app/features/channel/components/sticker_picker/StickerPickerSearchBar';
|
||||||
import {VirtualRowRenderer} from '@app/features/channel/components/sticker_picker/VirtualRow';
|
import {VirtualRowRenderer} from '@app/features/channel/components/sticker_picker/VirtualRow';
|
||||||
@@ -34,12 +40,13 @@ import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
|||||||
import {usePremiumUpsellData} from '@app/features/premium/hooks/usePremiumUpsellData';
|
import {usePremiumUpsellData} from '@app/features/premium/hooks/usePremiumUpsellData';
|
||||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||||
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||||
|
import {getAppRemScale} from '@app/features/ui/utils/AppZoomUtils';
|
||||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||||
import {msg} from '@lingui/core/macro';
|
import {msg} from '@lingui/core/macro';
|
||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro';
|
import {Plural, Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {SmileySadIcon, StickerIcon} from '@phosphor-icons/react';
|
import {SmileySadIcon, StickerIcon} from '@phosphor-icons/react';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import {useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore} from 'react';
|
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore} from 'react';
|
||||||
|
|
||||||
const NO_STICKERS_AVAILABLE_DESCRIPTOR = msg({
|
const NO_STICKERS_AVAILABLE_DESCRIPTOR = msg({
|
||||||
message: 'No stickers yet',
|
message: 'No stickers yet',
|
||||||
@@ -73,9 +80,10 @@ export const MobileStickersPicker = observer(
|
|||||||
const scrollerRef = useRef<ScrollerHandle>(null);
|
const scrollerRef = useRef<ScrollerHandle>(null);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const stickerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
const stickerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||||
const {viewportSize, handleResize} = useScrollerViewport(scrollerRef);
|
const {viewportSize, scrollTop, handleScroll, handleResize} = useScrollerViewport(scrollerRef);
|
||||||
const channel = channelId ? (Channels.getChannel(channelId) ?? null) : null;
|
const channel = channelId ? (Channels.getChannel(channelId) ?? null) : null;
|
||||||
const categoryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const rowListRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [listMetrics, setListMetrics] = useState({origin: 0, viewportHeight: 0, gridWidth: 0});
|
||||||
const [stickerDataVersion, setStickerDataVersion] = useState(0);
|
const [stickerDataVersion, setStickerDataVersion] = useState(0);
|
||||||
const permissionVersion = useSyncExternalStore(Permission.subscribe.bind(Permission), () => Permission.version);
|
const permissionVersion = useSyncExternalStore(Permission.subscribe.bind(Permission), () => Permission.version);
|
||||||
const {shouldAnimate: shouldAnimateStickerPreview} = useStickerAnimation();
|
const {shouldAnimate: shouldAnimateStickerPreview} = useStickerAnimation();
|
||||||
@@ -95,10 +103,10 @@ export const MobileStickersPicker = observer(
|
|||||||
src={AvatarUtils.getStickerURL({
|
src={AvatarUtils.getStickerURL({
|
||||||
id: sticker.id,
|
id: sticker.id,
|
||||||
animated: shouldAnimateStickerPreview,
|
animated: shouldAnimateStickerPreview,
|
||||||
|
isAnimatable: sticker.animated,
|
||||||
size: 320,
|
size: 320,
|
||||||
})}
|
})}
|
||||||
alt={sticker.name}
|
alt={sticker.name}
|
||||||
loading="lazy"
|
|
||||||
data-flx="channel.mobile-stickers-picker.render-sticker-preview-item.img"
|
data-flx="channel.mobile-stickers-picker.render-sticker-preview-item.img"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,11 +178,58 @@ export const MobileStickersPicker = observer(
|
|||||||
const isSearching = searchTerm.trim().length > 0;
|
const isSearching = searchTerm.trim().length > 0;
|
||||||
const showPremiumUpsell =
|
const showPremiumUpsell =
|
||||||
shouldShowPremiumFeatures() && shouldShowStickerPremiumUpsell(channel) && !isSearching && lockedStickerCount > 0;
|
shouldShowPremiumFeatures() && shouldShowStickerPremiumUpsell(channel) && !isSearching && lockedStickerCount > 0;
|
||||||
const handleCategoryClick = (category: string) => {
|
const {stickerRowIndexes, categoryRowIndexes} = useMemo(() => {
|
||||||
const element = categoryRefs.current.get(category);
|
const indexes = new Array<number>(pickerRows.length);
|
||||||
if (element) {
|
const categories = new Map<string, number>();
|
||||||
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
let stickerRowIndex = 0;
|
||||||
|
for (let rowIndex = 0; rowIndex < pickerRows.length; rowIndex += 1) {
|
||||||
|
const row = pickerRows[rowIndex];
|
||||||
|
indexes[rowIndex] = stickerRowIndex;
|
||||||
|
if (row.type === 'sticker-row') {
|
||||||
|
stickerRowIndex += 1;
|
||||||
|
} else {
|
||||||
|
categories.set(row.category, rowIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return {stickerRowIndexes: indexes, categoryRowIndexes: categories};
|
||||||
|
}, [pickerRows]);
|
||||||
|
const remScale = getAppRemScale();
|
||||||
|
const stickerRowHeight = useMemo(
|
||||||
|
() => getStickerRowHeight(listMetrics.gridWidth, gridColumns, remScale),
|
||||||
|
[listMetrics.gridWidth, gridColumns, remScale, zoomLevel],
|
||||||
|
);
|
||||||
|
const rowOffsets = useMemo(() => {
|
||||||
|
const rowKinds = pickerRows.map((row): StickerRowKind => row.type);
|
||||||
|
return buildStickerRowOffsets(rowKinds, {remScale, stickerRowHeight, sectionGap: 0});
|
||||||
|
}, [pickerRows, remScale, stickerRowHeight, zoomLevel]);
|
||||||
|
const contentHeight = rowOffsets[rowOffsets.length - 1]!;
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const rowListNode = rowListRef.current;
|
||||||
|
const scrollerNode = scrollerRef.current?.getViewportElement();
|
||||||
|
if (!rowListNode || !scrollerNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const origin = Math.round(
|
||||||
|
rowListNode.getBoundingClientRect().top - scrollerNode.getBoundingClientRect().top + scrollerNode.scrollTop,
|
||||||
|
);
|
||||||
|
const viewportHeight = scrollerNode.clientHeight;
|
||||||
|
const gridWidth = rowListNode.clientWidth;
|
||||||
|
setListMetrics((current) =>
|
||||||
|
current.origin === origin && current.viewportHeight === viewportHeight && current.gridWidth === gridWidth
|
||||||
|
? current
|
||||||
|
: {origin, viewportHeight, gridWidth},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const rowWindow = useMemo(
|
||||||
|
() => getStickerRowWindow(rowOffsets, scrollTop - listMetrics.origin, listMetrics.viewportHeight),
|
||||||
|
[rowOffsets, scrollTop, listMetrics],
|
||||||
|
);
|
||||||
|
const handleCategoryClick = (category: string) => {
|
||||||
|
const rowIndex = categoryRowIndexes.get(category);
|
||||||
|
if (rowIndex == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollerRef.current?.scrollTo({to: listMetrics.origin + rowOffsets[rowIndex]!});
|
||||||
};
|
};
|
||||||
const handleHover = (sticker: GuildSticker | null) => {
|
const handleHover = (sticker: GuildSticker | null) => {
|
||||||
setHoveredSticker(sticker);
|
setHoveredSticker(sticker);
|
||||||
@@ -252,6 +307,7 @@ export const MobileStickersPicker = observer(
|
|||||||
ref={scrollerRef}
|
ref={scrollerRef}
|
||||||
className={`${mobileStyles.list} ${mobileStyles.listWrapper}`}
|
className={`${mobileStyles.list} ${mobileStyles.listWrapper}`}
|
||||||
key="mobile-stickers-picker-scroller"
|
key="mobile-stickers-picker-scroller"
|
||||||
|
onScroll={handleScroll}
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
data-flx="channel.mobile-stickers-picker.scroller"
|
data-flx="channel.mobile-stickers-picker.scroller"
|
||||||
>
|
>
|
||||||
@@ -264,39 +320,41 @@ export const MobileStickersPicker = observer(
|
|||||||
data-flx="channel.mobile-stickers-picker.premium-upsell-banner"
|
data-flx="channel.mobile-stickers-picker.premium-upsell-banner"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pickerRows.map((row, index) => {
|
<div
|
||||||
const stickerRowIndex = pickerRows.slice(0, index).filter((r) => r.type === 'sticker-row').length;
|
ref={rowListRef}
|
||||||
return (
|
style={{position: 'relative', flexShrink: 0, height: `${contentHeight}px`}}
|
||||||
<div
|
data-flx="channel.mobile-stickers-picker.row-list"
|
||||||
key={`${row.type}-${row.index}`}
|
>
|
||||||
ref={
|
{pickerRows.slice(rowWindow.firstRow, rowWindow.lastRow + 1).map((row, windowIndex) => {
|
||||||
row.type === 'header'
|
const rowIndex = rowWindow.firstRow + windowIndex;
|
||||||
? (el) => {
|
return (
|
||||||
if (el && 'category' in row) {
|
<div
|
||||||
categoryRefs.current.set(row.category, el);
|
key={`${row.type}-${row.index}`}
|
||||||
}
|
style={{
|
||||||
}
|
position: 'absolute',
|
||||||
: undefined
|
left: 0,
|
||||||
}
|
right: 0,
|
||||||
data-flx="channel.mobile-stickers-picker.div--5"
|
transform: `translateY(${rowOffsets[rowIndex]!}px)`,
|
||||||
>
|
}}
|
||||||
<VirtualRowRenderer
|
data-flx="channel.mobile-stickers-picker.div--5"
|
||||||
row={row}
|
>
|
||||||
handleHover={handleHover}
|
<VirtualRowRenderer
|
||||||
handleSelect={handleStickerSelect}
|
row={row}
|
||||||
gridColumns={gridColumns}
|
handleHover={handleHover}
|
||||||
hoveredSticker={hoveredSticker}
|
handleSelect={handleStickerSelect}
|
||||||
selectedRow={-1}
|
gridColumns={gridColumns}
|
||||||
selectedColumn={-1}
|
selectedRow={-1}
|
||||||
stickerRowIndex={stickerRowIndex}
|
selectedColumn={-1}
|
||||||
shouldScrollOnSelection={false}
|
stickerRowIndex={stickerRowIndexes[rowIndex]!}
|
||||||
stickerRefs={stickerRefs}
|
shouldScrollOnSelection={false}
|
||||||
channel={channel}
|
stickerRefs={stickerRefs}
|
||||||
data-flx="channel.mobile-stickers-picker.virtual-row-renderer"
|
channel={channel}
|
||||||
/>
|
data-flx="channel.mobile-stickers-picker.virtual-row-renderer"
|
||||||
</div>
|
/>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</Scroller>
|
</Scroller>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||||
import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||||
import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAutofocus';
|
import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAutofocus';
|
||||||
import styles from '@app/features/channel/components/EmojiPicker.module.css';
|
import styles from '@app/features/channel/components/EmojiPicker.module.css';
|
||||||
import gifStyles from '@app/features/channel/components/GifPicker.module.css';
|
import gifStyles from '@app/features/channel/components/GifPicker.module.css';
|
||||||
import {PremiumUpsellBanner} from '@app/features/channel/components/PremiumUpsellBanner';
|
import {PremiumUpsellBanner} from '@app/features/channel/components/PremiumUpsellBanner';
|
||||||
import premiumStyles from '@app/features/channel/components/PremiumUpsellBanner.module.css';
|
import premiumStyles from '@app/features/channel/components/PremiumUpsellBanner.module.css';
|
||||||
|
import {useScrollerViewport} from '@app/features/channel/components/pickers/shared/useScrollerViewport';
|
||||||
import {PickerEmptyState} from '@app/features/channel/components/shared/PickerEmptyState';
|
import {PickerEmptyState} from '@app/features/channel/components/shared/PickerEmptyState';
|
||||||
import {useStickerCategories} from '@app/features/channel/components/sticker_picker/hooks/useStickerCategories';
|
import {useStickerCategories} from '@app/features/channel/components/sticker_picker/hooks/useStickerCategories';
|
||||||
import {useVirtualRows} from '@app/features/channel/components/sticker_picker/hooks/useVirtualRows';
|
import {useVirtualRows} from '@app/features/channel/components/sticker_picker/hooks/useVirtualRows';
|
||||||
import {StickerPickerCategoryList} from '@app/features/channel/components/sticker_picker/StickerPickerCategoryList';
|
import {StickerPickerCategoryList} from '@app/features/channel/components/sticker_picker/StickerPickerCategoryList';
|
||||||
import {STICKERS_PER_ROW} from '@app/features/channel/components/sticker_picker/StickerPickerConstants';
|
import {
|
||||||
|
buildStickerRowOffsets,
|
||||||
|
getStickerRowHeight,
|
||||||
|
getStickerRowWindow,
|
||||||
|
STICKER_SECTION_GAP,
|
||||||
|
STICKERS_PER_ROW,
|
||||||
|
type StickerRowKind,
|
||||||
|
} from '@app/features/channel/components/sticker_picker/StickerPickerConstants';
|
||||||
import {StickerPickerInspector} from '@app/features/channel/components/sticker_picker/StickerPickerInspector';
|
import {StickerPickerInspector} from '@app/features/channel/components/sticker_picker/StickerPickerInspector';
|
||||||
import {StickerPickerSearchBar} from '@app/features/channel/components/sticker_picker/StickerPickerSearchBar';
|
import {StickerPickerSearchBar} from '@app/features/channel/components/sticker_picker/StickerPickerSearchBar';
|
||||||
import {VirtualRowWrapper} from '@app/features/channel/components/sticker_picker/VirtualRow';
|
import {VirtualRowWrapper} from '@app/features/channel/components/sticker_picker/VirtualRow';
|
||||||
@@ -29,13 +38,14 @@ import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
|||||||
import {usePremiumUpsellData} from '@app/features/premium/hooks/usePremiumUpsellData';
|
import {usePremiumUpsellData} from '@app/features/premium/hooks/usePremiumUpsellData';
|
||||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||||
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
import {Scroller, type ScrollerHandle} from '@app/features/ui/components/Scroller';
|
||||||
|
import {getAppRemScale} from '@app/features/ui/utils/AppZoomUtils';
|
||||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||||
import {msg} from '@lingui/core/macro';
|
import {msg} from '@lingui/core/macro';
|
||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro';
|
import {Plural, Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {SmileySadIcon, StickerIcon} from '@phosphor-icons/react';
|
import {SmileySadIcon, StickerIcon} 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 {useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore} from 'react';
|
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore} from 'react';
|
||||||
|
|
||||||
const NO_STICKERS_AVAILABLE_DESCRIPTOR = msg({
|
const NO_STICKERS_AVAILABLE_DESCRIPTOR = msg({
|
||||||
message: 'No stickers yet',
|
message: 'No stickers yet',
|
||||||
@@ -64,10 +74,12 @@ export const StickersPicker = observer(
|
|||||||
const [selectedColumn, setSelectedColumn] = useState(-1);
|
const [selectedColumn, setSelectedColumn] = useState(-1);
|
||||||
const [shouldScrollOnSelection, setShouldScrollOnSelection] = useState(false);
|
const [shouldScrollOnSelection, setShouldScrollOnSelection] = useState(false);
|
||||||
const scrollerRef = useRef<ScrollerHandle>(null);
|
const scrollerRef = useRef<ScrollerHandle>(null);
|
||||||
|
const {scrollTop, handleScroll, handleResize} = useScrollerViewport(scrollerRef);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const stickerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
const stickerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||||
const channel = channelId ? (Channels.getChannel(channelId) ?? null) : null;
|
const channel = channelId ? (Channels.getChannel(channelId) ?? null) : null;
|
||||||
const categoryRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const rowListRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [listMetrics, setListMetrics] = useState({origin: 0, viewportHeight: 0, gridWidth: 0});
|
||||||
const [stickerDataVersion, setStickerDataVersion] = useState(0);
|
const [stickerDataVersion, setStickerDataVersion] = useState(0);
|
||||||
const permissionVersion = useSyncExternalStore(Permission.subscribe.bind(Permission), () => Permission.version);
|
const permissionVersion = useSyncExternalStore(Permission.subscribe.bind(Permission), () => Permission.version);
|
||||||
const {shouldAnimate: shouldAnimateStickerPreview} = useStickerAnimation();
|
const {shouldAnimate: shouldAnimateStickerPreview} = useStickerAnimation();
|
||||||
@@ -87,10 +99,10 @@ export const StickersPicker = observer(
|
|||||||
src={AvatarUtils.getStickerURL({
|
src={AvatarUtils.getStickerURL({
|
||||||
id: sticker.id,
|
id: sticker.id,
|
||||||
animated: shouldAnimateStickerPreview,
|
animated: shouldAnimateStickerPreview,
|
||||||
|
isAnimatable: sticker.animated,
|
||||||
size: 320,
|
size: 320,
|
||||||
})}
|
})}
|
||||||
alt={sticker.name}
|
alt={sticker.name}
|
||||||
loading="lazy"
|
|
||||||
data-flx="channel.stickers-picker.render-sticker-preview-item.img"
|
data-flx="channel.stickers-picker.render-sticker-preview-item.img"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,17 +181,61 @@ export const StickersPicker = observer(
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [pickerRows]);
|
}, [pickerRows]);
|
||||||
|
const {stickerRowIndexes, stickerRowStarts, categoryRowIndexes} = useMemo(() => {
|
||||||
|
const indexes = new Array<number>(pickerRows.length);
|
||||||
|
const starts: Array<number> = [];
|
||||||
|
const categories = new Map<string, number>();
|
||||||
|
let stickerRowIndex = 0;
|
||||||
|
for (let rowIndex = 0; rowIndex < pickerRows.length; rowIndex += 1) {
|
||||||
|
const row = pickerRows[rowIndex];
|
||||||
|
indexes[rowIndex] = stickerRowIndex;
|
||||||
|
if (row.type === 'sticker-row') {
|
||||||
|
starts.push(rowIndex);
|
||||||
|
stickerRowIndex += 1;
|
||||||
|
} else {
|
||||||
|
categories.set(row.category, rowIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {stickerRowIndexes: indexes, stickerRowStarts: starts, categoryRowIndexes: categories};
|
||||||
|
}, [pickerRows]);
|
||||||
|
const zoomLevel = Accessibility.zoomLevel;
|
||||||
|
const remScale = getAppRemScale();
|
||||||
|
const stickerRowHeight = useMemo(
|
||||||
|
() => getStickerRowHeight(listMetrics.gridWidth, STICKERS_PER_ROW, remScale),
|
||||||
|
[listMetrics.gridWidth, remScale, zoomLevel],
|
||||||
|
);
|
||||||
|
const rowOffsets = useMemo(() => {
|
||||||
|
const rowKinds = pickerRows.map((row): StickerRowKind => row.type);
|
||||||
|
return buildStickerRowOffsets(rowKinds, {remScale, stickerRowHeight, sectionGap: STICKER_SECTION_GAP});
|
||||||
|
}, [pickerRows, remScale, stickerRowHeight, zoomLevel]);
|
||||||
|
const contentHeight = rowOffsets[rowOffsets.length - 1]!;
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const rowListNode = rowListRef.current;
|
||||||
|
const scrollerNode = scrollerRef.current?.getViewportElement();
|
||||||
|
if (!rowListNode || !scrollerNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const origin = Math.round(
|
||||||
|
rowListNode.getBoundingClientRect().top - scrollerNode.getBoundingClientRect().top + scrollerNode.scrollTop,
|
||||||
|
);
|
||||||
|
const viewportHeight = scrollerNode.clientHeight;
|
||||||
|
const gridWidth = rowListNode.clientWidth;
|
||||||
|
setListMetrics((current) =>
|
||||||
|
current.origin === origin && current.viewportHeight === viewportHeight && current.gridWidth === gridWidth
|
||||||
|
? current
|
||||||
|
: {origin, viewportHeight, gridWidth},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const rowWindow = useMemo(
|
||||||
|
() => getStickerRowWindow(rowOffsets, scrollTop - listMetrics.origin, listMetrics.viewportHeight),
|
||||||
|
[rowOffsets, scrollTop, listMetrics],
|
||||||
|
);
|
||||||
const handleCategoryClick = (category: string) => {
|
const handleCategoryClick = (category: string) => {
|
||||||
const element = categoryRefs.current.get(category);
|
const rowIndex = categoryRowIndexes.get(category);
|
||||||
if (element) {
|
if (rowIndex == null) {
|
||||||
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
return;
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleHover = (sticker: GuildSticker | null, row?: number, column?: number) => {
|
|
||||||
setHoveredSticker(sticker);
|
|
||||||
if (sticker && row !== undefined && column !== undefined) {
|
|
||||||
handleSelectionChange(row, column, false);
|
|
||||||
}
|
}
|
||||||
|
scrollerRef.current?.scrollTo({to: listMetrics.origin + rowOffsets[rowIndex]!});
|
||||||
};
|
};
|
||||||
const handleStickerSelect = useCallback(
|
const handleStickerSelect = useCallback(
|
||||||
(sticker: GuildSticker, shiftKey?: boolean) => {
|
(sticker: GuildSticker, shiftKey?: boolean) => {
|
||||||
@@ -214,11 +270,31 @@ export const StickersPicker = observer(
|
|||||||
},
|
},
|
||||||
[pickerRows],
|
[pickerRows],
|
||||||
);
|
);
|
||||||
|
const handleHover = useCallback(
|
||||||
|
(sticker: GuildSticker | null, row?: number, column?: number) => {
|
||||||
|
setHoveredSticker(sticker);
|
||||||
|
if (sticker && row !== undefined && column !== undefined) {
|
||||||
|
handleSelectionChange(row, column, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSelectionChange],
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (renderedStickers.length > 0 && selectedRow === 0 && selectedColumn === 0 && !hoveredSticker) {
|
if (renderedStickers.length > 0 && selectedRow === 0 && selectedColumn === 0 && !hoveredSticker) {
|
||||||
handleSelectionChange(0, 0, false);
|
handleSelectionChange(0, 0, false);
|
||||||
}
|
}
|
||||||
}, [renderedStickers, selectedRow, selectedColumn, hoveredSticker, handleSelectionChange]);
|
}, [renderedStickers, selectedRow, selectedColumn, hoveredSticker, handleSelectionChange]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldScrollOnSelection || selectedRow < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rowIndex = stickerRowStarts[selectedRow];
|
||||||
|
if (rowIndex == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const start = listMetrics.origin + rowOffsets[rowIndex]!;
|
||||||
|
scrollerRef.current?.revealRange({start, end: start + stickerRowHeight});
|
||||||
|
}, [shouldScrollOnSelection, selectedRow, stickerRowStarts, rowOffsets, listMetrics.origin, stickerRowHeight]);
|
||||||
const handleSelectSticker = useCallback(
|
const handleSelectSticker = useCallback(
|
||||||
(row: number | null, column: number | null, event?: React.KeyboardEvent) => {
|
(row: number | null, column: number | null, event?: React.KeyboardEvent) => {
|
||||||
if (row === null || column === null) {
|
if (row === null || column === null) {
|
||||||
@@ -283,6 +359,8 @@ export const StickersPicker = observer(
|
|||||||
className={`${styles.list} ${styles.listWrapper}`}
|
className={`${styles.list} ${styles.listWrapper}`}
|
||||||
fade={false}
|
fade={false}
|
||||||
key="stickers-picker-scroller"
|
key="stickers-picker-scroller"
|
||||||
|
onScroll={handleScroll}
|
||||||
|
onResize={handleResize}
|
||||||
data-flx="channel.stickers-picker.list"
|
data-flx="channel.stickers-picker.list"
|
||||||
>
|
>
|
||||||
{showPremiumUpsell && (
|
{showPremiumUpsell && (
|
||||||
@@ -294,41 +372,41 @@ export const StickersPicker = observer(
|
|||||||
data-flx="channel.stickers-picker.premium-upsell-banner"
|
data-flx="channel.stickers-picker.premium-upsell-banner"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pickerRows.map((row, index) => {
|
<div
|
||||||
const stickerRowIndex = pickerRows.slice(0, index).filter((r) => r.type === 'sticker-row').length;
|
ref={rowListRef}
|
||||||
const needsSpacingAfter = row.type === 'sticker-row' && pickerRows[index + 1]?.type === 'header';
|
style={{position: 'relative', flexShrink: 0, height: `${contentHeight}px`}}
|
||||||
return (
|
data-flx="channel.stickers-picker.row-list"
|
||||||
<div
|
>
|
||||||
key={`${row.type}-${row.index}`}
|
{pickerRows.slice(rowWindow.firstRow, rowWindow.lastRow + 1).map((row, windowIndex) => {
|
||||||
ref={
|
const rowIndex = rowWindow.firstRow + windowIndex;
|
||||||
row.type === 'header'
|
return (
|
||||||
? (el) => {
|
<div
|
||||||
if (el && 'category' in row) {
|
key={`${row.type}-${row.index}`}
|
||||||
categoryRefs.current.set(row.category, el);
|
style={{
|
||||||
}
|
position: 'absolute',
|
||||||
}
|
left: 0,
|
||||||
: undefined
|
right: 0,
|
||||||
}
|
transform: `translateY(${rowOffsets[rowIndex]!}px)`,
|
||||||
style={row.type === 'sticker-row' && needsSpacingAfter ? {marginBottom: '12px'} : undefined}
|
}}
|
||||||
data-flx="channel.stickers-picker.div--3"
|
data-flx="channel.stickers-picker.div--3"
|
||||||
>
|
>
|
||||||
<VirtualRowWrapper
|
<VirtualRowWrapper
|
||||||
row={row}
|
row={row}
|
||||||
handleHover={handleHover}
|
handleHover={handleHover}
|
||||||
handleSelect={handleStickerSelect}
|
handleSelect={handleStickerSelect}
|
||||||
gridColumns={STICKERS_PER_ROW}
|
gridColumns={STICKERS_PER_ROW}
|
||||||
hoveredSticker={hoveredSticker}
|
selectedRow={selectedRow}
|
||||||
selectedRow={selectedRow}
|
selectedColumn={selectedColumn}
|
||||||
selectedColumn={selectedColumn}
|
stickerRowIndex={stickerRowIndexes[rowIndex]!}
|
||||||
stickerRowIndex={stickerRowIndex}
|
shouldScrollOnSelection={shouldScrollOnSelection}
|
||||||
shouldScrollOnSelection={shouldScrollOnSelection}
|
stickerRefs={stickerRefs}
|
||||||
stickerRefs={stickerRefs}
|
channel={channel}
|
||||||
channel={channel}
|
data-flx="channel.stickers-picker.virtual-row-wrapper"
|
||||||
data-flx="channel.stickers-picker.virtual-row-wrapper"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</Scroller>
|
</Scroller>
|
||||||
{renderedStickers.length === 0 && (
|
{renderedStickers.length === 0 && (
|
||||||
<div className={styles.emptyState} data-flx="channel.stickers-picker.empty-state">
|
<div className={styles.emptyState} data-flx="channel.stickers-picker.empty-state">
|
||||||
|
|||||||
@@ -2,3 +2,91 @@
|
|||||||
|
|
||||||
export const STICKERS_PER_ROW = 4;
|
export const STICKERS_PER_ROW = 4;
|
||||||
export const STICKERS_PER_ROW_MOBILE = 5;
|
export const STICKERS_PER_ROW_MOBILE = 5;
|
||||||
|
export const STICKER_CATEGORY_HEADER_HEIGHT = 32;
|
||||||
|
export const STICKER_SECTION_GAP = 12;
|
||||||
|
|
||||||
|
const STICKER_CATEGORY_HEADER_GAP = 8;
|
||||||
|
const STICKER_GRID_GAP = 8;
|
||||||
|
const STICKER_GRID_BLOCK_END_PADDING = 8;
|
||||||
|
const STICKER_ROW_WINDOW_LEAD_ROWS = 2;
|
||||||
|
|
||||||
|
export type StickerRowKind = 'header' | 'sticker-row';
|
||||||
|
|
||||||
|
export interface StickerRowMetrics {
|
||||||
|
remScale: number;
|
||||||
|
stickerRowHeight: number;
|
||||||
|
sectionGap: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StickerRowWindow {
|
||||||
|
firstRow: number;
|
||||||
|
lastRow: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStickerRowHeight(gridWidth: number, gridColumns: number, remScale: number): number {
|
||||||
|
if (!(gridWidth > 0) || gridColumns <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const cellWidth = Math.max(0, (gridWidth - STICKER_GRID_GAP * remScale * (gridColumns - 1)) / gridColumns);
|
||||||
|
return cellWidth + STICKER_GRID_BLOCK_END_PADDING * remScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStickerRowOffsets(
|
||||||
|
rowKinds: ReadonlyArray<StickerRowKind>,
|
||||||
|
{remScale, stickerRowHeight, sectionGap}: StickerRowMetrics,
|
||||||
|
): Array<number> {
|
||||||
|
const rowOffsets = new Array<number>(rowKinds.length + 1);
|
||||||
|
rowOffsets[0] = 0;
|
||||||
|
let runningOffset = 0;
|
||||||
|
for (let rowIndex = 0; rowIndex < rowKinds.length; rowIndex += 1) {
|
||||||
|
if (rowKinds[rowIndex] === 'header') {
|
||||||
|
runningOffset += (STICKER_CATEGORY_HEADER_HEIGHT + STICKER_CATEGORY_HEADER_GAP) * remScale;
|
||||||
|
} else {
|
||||||
|
runningOffset += stickerRowHeight;
|
||||||
|
if (rowKinds[rowIndex + 1] === 'header') {
|
||||||
|
runningOffset += sectionGap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowOffsets[rowIndex + 1] = runningOffset;
|
||||||
|
}
|
||||||
|
return rowOffsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findStickerRowForOffset(rowOffsets: ReadonlyArray<number>, pixel: number): number {
|
||||||
|
const lastRow = rowOffsets.length - 2;
|
||||||
|
if (lastRow < 0 || pixel <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (pixel >= rowOffsets[lastRow + 1]!) {
|
||||||
|
return lastRow;
|
||||||
|
}
|
||||||
|
let low = 0;
|
||||||
|
let high = lastRow;
|
||||||
|
while (low < high) {
|
||||||
|
const mid = (low + high + 1) >> 1;
|
||||||
|
if (rowOffsets[mid]! <= pixel) {
|
||||||
|
low = mid;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStickerRowWindow(
|
||||||
|
rowOffsets: ReadonlyArray<number>,
|
||||||
|
scrollTop: number,
|
||||||
|
viewportHeight: number,
|
||||||
|
): StickerRowWindow {
|
||||||
|
const totalRows = rowOffsets.length - 1;
|
||||||
|
if (totalRows <= 0 || !(viewportHeight > 0)) {
|
||||||
|
return {firstRow: 0, lastRow: -1};
|
||||||
|
}
|
||||||
|
const safeScrollTop = Number.isFinite(scrollTop) ? Math.max(0, scrollTop) : 0;
|
||||||
|
const leadingVisibleRow = findStickerRowForOffset(rowOffsets, safeScrollTop);
|
||||||
|
const trailingVisibleRow = findStickerRowForOffset(rowOffsets, safeScrollTop + viewportHeight);
|
||||||
|
return {
|
||||||
|
firstRow: Math.max(0, leadingVisibleRow - STICKER_ROW_WINDOW_LEAD_ROWS),
|
||||||
|
lastRow: Math.min(totalRows - 1, trailingVisibleRow + STICKER_ROW_WINDOW_LEAD_ROWS),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import type {VirtualRow} from '@app/features/channel/components/sticker_picker/hooks/useVirtualRows';
|
import type {VirtualRow} from '@app/features/channel/components/sticker_picker/hooks/useVirtualRows';
|
||||||
|
import {STICKER_CATEGORY_HEADER_HEIGHT} from '@app/features/channel/components/sticker_picker/StickerPickerConstants';
|
||||||
import styles from '@app/features/channel/components/sticker_picker/VirtualRow.module.css';
|
import styles from '@app/features/channel/components/sticker_picker/VirtualRow.module.css';
|
||||||
import type {Channel} from '@app/features/channel/models/Channel';
|
import type {Channel} from '@app/features/channel/models/Channel';
|
||||||
import * as StickerPickerCommands from '@app/features/emoji/commands/StickerPickerCommands';
|
import * as StickerPickerCommands from '@app/features/emoji/commands/StickerPickerCommands';
|
||||||
@@ -19,18 +20,13 @@ import {useLingui} from '@lingui/react/macro';
|
|||||||
import {CaretDownIcon, ClockIcon, StarIcon} from '@phosphor-icons/react';
|
import {CaretDownIcon, ClockIcon, StarIcon} from '@phosphor-icons/react';
|
||||||
import {clsx} from 'clsx';
|
import {clsx} from 'clsx';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const STICKER_ROW_HEIGHT = 92;
|
|
||||||
const CATEGORY_HEADER_HEIGHT = 32;
|
|
||||||
const OVERSCAN_ROWS = 5;
|
|
||||||
|
|
||||||
interface VirtualRowRendererProps {
|
interface VirtualRowRendererProps {
|
||||||
row: VirtualRow;
|
row: VirtualRow;
|
||||||
handleHover: (sticker: GuildSticker | null, row?: number, column?: number) => void;
|
handleHover: (sticker: GuildSticker | null, row?: number, column?: number) => void;
|
||||||
handleSelect: (sticker: GuildSticker, shiftKey?: boolean) => void;
|
handleSelect: (sticker: GuildSticker, shiftKey?: boolean) => void;
|
||||||
gridColumns?: number;
|
gridColumns?: number;
|
||||||
hoveredSticker: GuildSticker | null;
|
|
||||||
selectedRow: number;
|
selectedRow: number;
|
||||||
selectedColumn: number;
|
selectedColumn: number;
|
||||||
stickerRowIndex: number;
|
stickerRowIndex: number;
|
||||||
@@ -65,7 +61,9 @@ const StickerButton: React.FC<StickerButtonProps> = React.memo(
|
|||||||
stickerRowIndex,
|
stickerRowIndex,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
}) => {
|
}) => {
|
||||||
const {shouldAnimate, interactionHandlers} = useStickerAnimation({respectUserSettings: false});
|
const {shouldAnimate, interactionHandlers} = useStickerAnimation({
|
||||||
|
isAnimated: sticker.animated,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<FocusRing offset={-2} data-flx="channel.sticker-picker.virtual-row.sticker-button.focus-ring">
|
<FocusRing offset={-2} data-flx="channel.sticker-picker.virtual-row.sticker-button.focus-ring">
|
||||||
<button
|
<button
|
||||||
@@ -107,11 +105,11 @@ const StickerButton: React.FC<StickerButtonProps> = React.memo(
|
|||||||
src={AvatarUtils.getStickerURL({
|
src={AvatarUtils.getStickerURL({
|
||||||
id: sticker.id,
|
id: sticker.id,
|
||||||
animated: shouldAnimate,
|
animated: shouldAnimate,
|
||||||
|
isAnimatable: sticker.animated,
|
||||||
size: 320,
|
size: 320,
|
||||||
})}
|
})}
|
||||||
alt={sticker.name}
|
alt={sticker.name}
|
||||||
className={styles.stickerImage}
|
className={styles.stickerImage}
|
||||||
loading="lazy"
|
|
||||||
data-flx="channel.sticker-picker.virtual-row.sticker-button.sticker-image"
|
data-flx="channel.sticker-picker.virtual-row.sticker-button.sticker-image"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@@ -175,7 +173,7 @@ const VirtualRowRendererBase: React.FC<VirtualRowRendererProps> = React.memo(
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggleCategory}
|
onClick={handleToggleCategory}
|
||||||
style={{
|
style={{
|
||||||
height: remFromPx(CATEGORY_HEADER_HEIGHT),
|
height: remFromPx(STICKER_CATEGORY_HEADER_HEIGHT),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingLeft: '0.75rem',
|
paddingLeft: '0.75rem',
|
||||||
@@ -288,7 +286,6 @@ interface VirtualRowWrapperProps {
|
|||||||
handleHover: (sticker: GuildSticker | null, row?: number, column?: number) => void;
|
handleHover: (sticker: GuildSticker | null, row?: number, column?: number) => void;
|
||||||
handleSelect: (sticker: GuildSticker, shiftKey?: boolean) => void;
|
handleSelect: (sticker: GuildSticker, shiftKey?: boolean) => void;
|
||||||
gridColumns?: number;
|
gridColumns?: number;
|
||||||
hoveredSticker: GuildSticker | null;
|
|
||||||
selectedRow: number;
|
selectedRow: number;
|
||||||
selectedColumn: number;
|
selectedColumn: number;
|
||||||
stickerRowIndex: number;
|
stickerRowIndex: number;
|
||||||
@@ -303,7 +300,6 @@ export const VirtualRowWrapper: React.FC<VirtualRowWrapperProps> = observer(
|
|||||||
handleHover,
|
handleHover,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
gridColumns,
|
gridColumns,
|
||||||
hoveredSticker,
|
|
||||||
selectedRow,
|
selectedRow,
|
||||||
selectedColumn,
|
selectedColumn,
|
||||||
stickerRowIndex,
|
stickerRowIndex,
|
||||||
@@ -311,63 +307,20 @@ export const VirtualRowWrapper: React.FC<VirtualRowWrapperProps> = observer(
|
|||||||
stickerRefs,
|
stickerRefs,
|
||||||
channel,
|
channel,
|
||||||
}) => {
|
}) => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
const placeholderRef = useRef<HTMLDivElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const placeholder = placeholderRef.current;
|
|
||||||
if (!placeholder) return;
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setIsVisible(true);
|
|
||||||
} else {
|
|
||||||
const rect = entry.boundingClientRect;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const overscanDistance = OVERSCAN_ROWS * STICKER_ROW_HEIGHT;
|
|
||||||
if (rect.bottom < -overscanDistance || rect.top > viewportHeight + overscanDistance) {
|
|
||||||
setIsVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rootMargin: `${OVERSCAN_ROWS * STICKER_ROW_HEIGHT}px 0px`,
|
|
||||||
threshold: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
observer.observe(placeholder);
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
const height = row.type === 'header' ? CATEGORY_HEADER_HEIGHT : STICKER_ROW_HEIGHT;
|
|
||||||
if (!isVisible) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={placeholderRef}
|
|
||||||
style={{height: remFromPx(height)}}
|
|
||||||
data-flx="channel.sticker-picker.virtual-row.virtual-row-wrapper.div"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div ref={placeholderRef} data-flx="channel.sticker-picker.virtual-row.virtual-row-wrapper.div--2">
|
<VirtualRowRenderer
|
||||||
<VirtualRowRenderer
|
row={row}
|
||||||
row={row}
|
handleHover={handleHover}
|
||||||
handleHover={handleHover}
|
handleSelect={handleSelect}
|
||||||
handleSelect={handleSelect}
|
gridColumns={gridColumns}
|
||||||
gridColumns={gridColumns}
|
selectedRow={selectedRow}
|
||||||
hoveredSticker={hoveredSticker}
|
selectedColumn={selectedColumn}
|
||||||
selectedRow={selectedRow}
|
stickerRowIndex={stickerRowIndex}
|
||||||
selectedColumn={selectedColumn}
|
shouldScrollOnSelection={shouldScrollOnSelection}
|
||||||
stickerRowIndex={stickerRowIndex}
|
stickerRefs={stickerRefs}
|
||||||
shouldScrollOnSelection={shouldScrollOnSelection}
|
channel={channel}
|
||||||
stickerRefs={stickerRefs}
|
data-flx="channel.sticker-picker.virtual-row.virtual-row-wrapper.virtual-row-renderer"
|
||||||
channel={channel}
|
/>
|
||||||
data-flx="channel.sticker-picker.virtual-row.virtual-row-wrapper.virtual-row-renderer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user