add virtualization in emoji board groups

This commit is contained in:
Ajay Bura 2025-09-14 11:24:52 +05:30
parent 90d9d4243e
commit 012e894d7a

View file

@ -2,27 +2,28 @@ import React, {
ChangeEventHandler, ChangeEventHandler,
FocusEventHandler, FocusEventHandler,
MouseEventHandler, MouseEventHandler,
UIEventHandler, ReactNode,
RefObject,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import { Box, Icons, Scroll } from 'folds'; import { Box, config, Icons, Scroll } from 'folds';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import { MatrixClient, Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import { Atom, atom, useAtomValue, useSetAtom } from 'jotai'; import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai';
import { useVirtualizer } from '@tanstack/react-virtual';
import { IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji'; import { IEmoji, emojiGroups, emojis } from '../../plugins/emoji';
import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels'; import { useEmojiGroupLabels } from './useEmojiGroupLabels';
import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons'; import { useEmojiGroupIcons } from './useEmojiGroupIcons';
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard'; import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
import { useRelevantImagePacks } from '../../hooks/useImagePacks'; import { useRelevantImagePacks } from '../../hooks/useImagePacks';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../hooks/useRecentEmoji'; import { useRecentEmoji } from '../../hooks/useRecentEmoji';
import { isUserId, mxcUrlToHttp } from '../../utils/matrix'; import { isUserId, mxcUrlToHttp } from '../../utils/matrix';
import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom'; import { editableActiveElement, targetFromEvent } from '../../utils/dom';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce'; import { useDebounce } from '../../hooks/useDebounce';
import { useThrottle } from '../../hooks/useThrottle'; import { useThrottle } from '../../hooks/useThrottle';
@ -46,36 +47,141 @@ import {
ImageGroupIcon, ImageGroupIcon,
GroupIcon, GroupIcon,
getEmojiItemInfo, getEmojiItemInfo,
getDOMGroupId,
EmojiGroup, EmojiGroup,
EmojiBoardLayout, EmojiBoardLayout,
} from './components'; } from './components';
import { EmojiBoardTab, EmojiType } from './types'; import { EmojiBoardTab, EmojiType } from './types';
import { VirtualTile } from '../virtualizer';
const RECENT_GROUP_ID = 'recent_group'; const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group'; const SEARCH_GROUP_ID = 'search_group';
type EmojiSidebarProps = { type EmojiGroupItem = {
activeGroupAtom: Atom<string | undefined>; id: string;
handleOpenGroup: (groupId: string) => void; name: string;
packs: ImagePack[]; items: Array<IEmoji | PackImageReader>;
groups: IEmojiGroup[];
icons: IEmojiGroupIcons;
labels: IEmojiGroupLabels;
}; };
function EmojiSidebar({ type StickerGroupItem = {
activeGroupAtom, id: string;
handleOpenGroup, name: string;
packs, items: Array<PackImageReader>;
groups, };
icons,
labels, const useGroups = (
}: EmojiSidebarProps) { tab: EmojiBoardTab,
imagePacks: ImagePack[]
): [EmojiGroupItem[], StickerGroupItem[]] => {
const mx = useMatrixClient();
const recentEmojis = useRecentEmoji(mx, 21);
const labels = useEmojiGroupLabels();
const emojiGroupItems = useMemo(() => {
const g: EmojiGroupItem[] = [];
if (tab !== EmojiBoardTab.Emoji) return g;
g.push({
id: RECENT_GROUP_ID,
name: 'Recent',
items: recentEmojis,
});
imagePacks.forEach((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
g.push({
id: pack.id,
name: label ?? 'Unknown',
items: pack
.getImages(ImageUsage.Emoticon)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode)),
});
});
emojiGroups.forEach((group) => {
g.push({
id: group.id,
name: labels[group.id],
items: group.emojis,
});
});
return g;
}, [mx, recentEmojis, labels, imagePacks, tab]);
const stickerGroupItems = useMemo(() => {
const g: StickerGroupItem[] = [];
if (tab !== EmojiBoardTab.Sticker) return g;
imagePacks.forEach((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
g.push({
id: pack.id,
name: label ?? 'Unknown',
items: pack
.getImages(ImageUsage.Sticker)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode)),
});
});
return g;
}, [mx, imagePacks, tab]);
return [emojiGroupItems, stickerGroupItems];
};
const useItemRenderer = (tab: EmojiBoardTab) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const activeGroupId = useAtomValue(activeGroupAtom); const renderItem = (emoji: IEmoji | PackImageReader, index: number) => {
if ('unicode' in emoji) {
return <EmojiItem key={emoji.unicode + index} emoji={emoji} />;
}
if (tab === EmojiBoardTab.Sticker) {
return (
<StickerItem
key={emoji.shortcode + index}
mx={mx}
useAuthentication={useAuthentication}
image={emoji}
/>
);
}
return (
<CustomEmojiItem
key={emoji.shortcode + index}
mx={mx}
useAuthentication={useAuthentication}
image={emoji}
/>
);
};
return renderItem;
};
type EmojiSidebarProps = {
activeGroupAtom: PrimitiveAtom<string | undefined>;
packs: ImagePack[];
onScrollToGroup: (groupId: string) => void;
};
function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom);
const usage = ImageUsage.Emoticon; const usage = ImageUsage.Emoticon;
const labels = useEmojiGroupLabels();
const icons = useEmojiGroupIcons();
const handleScrollToGroup = (groupId: string) => {
setActiveGroupId(groupId);
onScrollToGroup(groupId);
};
return ( return (
<Sidebar> <Sidebar>
@ -85,7 +191,7 @@ function EmojiSidebar({
id={RECENT_GROUP_ID} id={RECENT_GROUP_ID}
label="Recent" label="Recent"
icon={Icons.RecentClock} icon={Icons.RecentClock}
onClick={handleOpenGroup} onClick={handleScrollToGroup}
/> />
</SidebarStack> </SidebarStack>
{packs.length > 0 && ( {packs.length > 0 && (
@ -106,7 +212,7 @@ function EmojiSidebar({
id={pack.id} id={pack.id}
label={label ?? 'Unknown Pack'} label={label ?? 'Unknown Pack'}
url={url} url={url}
onClick={handleOpenGroup} onClick={handleScrollToGroup}
/> />
); );
})} })}
@ -120,14 +226,14 @@ function EmojiSidebar({
}} }}
> >
<SidebarDivider /> <SidebarDivider />
{groups.map((group) => ( {emojiGroups.map((group) => (
<GroupIcon <GroupIcon
key={group.id} key={group.id}
active={activeGroupId === group.id} active={activeGroupId === group.id}
id={group.id} id={group.id}
label={labels[group.id]} label={labels[group.id]}
icon={icons[group.id]} icon={icons[group.id]}
onClick={handleOpenGroup} onClick={handleScrollToGroup}
/> />
))} ))}
</SidebarStack> </SidebarStack>
@ -136,21 +242,25 @@ function EmojiSidebar({
} }
type StickerSidebarProps = { type StickerSidebarProps = {
activeGroupAtom: Atom<string | undefined>; activeGroupAtom: PrimitiveAtom<string | undefined>;
handleOpenGroup: (groupId: string) => void;
packs: ImagePack[]; packs: ImagePack[];
onScrollToGroup: (groupId: string) => void;
}; };
function StickerSidebar({ activeGroupAtom, handleOpenGroup, packs }: StickerSidebarProps) { function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSidebarProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const activeGroupId = useAtomValue(activeGroupAtom); const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom);
const usage = ImageUsage.Sticker; const usage = ImageUsage.Sticker;
const handleScrollToGroup = (groupId: string) => {
setActiveGroupId(groupId);
onScrollToGroup(groupId);
};
return ( return (
<Sidebar> <Sidebar>
<SidebarStack> <SidebarStack>
<SidebarDivider />
{packs.map((pack) => { {packs.map((pack) => {
let label = pack.meta.name; let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
@ -165,7 +275,7 @@ function StickerSidebar({ activeGroupAtom, handleOpenGroup, packs }: StickerSide
id={pack.id} id={pack.id}
label={label ?? 'Unknown Pack'} label={label ?? 'Unknown Pack'}
url={url} url={url}
onClick={handleOpenGroup} onClick={handleScrollToGroup}
/> />
); );
})} })}
@ -174,248 +284,19 @@ function StickerSidebar({ activeGroupAtom, handleOpenGroup, packs }: StickerSide
); );
} }
export function SearchGroup({ type EmojiGroupHolderProps = {
mx, contentScrollRef: RefObject<HTMLDivElement>;
tab, previewAtom: PrimitiveAtom<PreviewData | undefined>;
label, children?: ReactNode;
id, onGroupItemClick: MouseEventHandler;
emojis: searchResult,
useAuthentication,
}: {
mx: MatrixClient;
tab: EmojiBoardTab;
label: string;
id: string;
emojis: Array<PackImageReader | IEmoji>;
useAuthentication?: boolean;
}) {
return (
<EmojiGroup key={id} id={id} label={label}>
{searchResult.map((emoji) => {
if ('unicode' in emoji) {
return <EmojiItem key={emoji.unicode} emoji={emoji} />;
}
if (tab === EmojiBoardTab.Sticker) {
return (
<StickerItem
key={emoji.shortcode}
mx={mx}
useAuthentication={useAuthentication}
image={emoji}
/>
);
}
return (
<CustomEmojiItem
key={emoji.shortcode}
mx={mx}
useAuthentication={useAuthentication}
image={emoji}
/>
);
})}
</EmojiGroup>
);
}
type StickerGroupsProps = {
packs: ImagePack[];
}; };
function StickerGroups({ packs }: StickerGroupsProps) { function EmojiGroupHolder({
const mx = useMatrixClient(); contentScrollRef,
const useAuthentication = useMediaAuthentication(); previewAtom,
onGroupItemClick,
if (packs.length === 0) { children,
return <NoStickerPacks />; }: EmojiGroupHolderProps) {
}
return packs.map((pack) => (
<EmojiGroup key={pack.id} id={pack.id} label={pack.meta.name || 'Unknown'}>
{pack
.getImages(ImageUsage.Sticker)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode))
.map((image) => (
<StickerItem
key={image.shortcode}
mx={mx}
useAuthentication={useAuthentication}
image={image}
/>
))}
</EmojiGroup>
));
}
type EmojiGroupsProps = {
recentEmojis: IEmoji[];
packs: ImagePack[];
groups: IEmojiGroup[];
labels: IEmojiGroupLabels;
};
export function EmojiGroups({ recentEmojis, packs, groups, labels }: EmojiGroupsProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
return (
<>
<EmojiGroup id={RECENT_GROUP_ID} label="Recent">
{recentEmojis.map((emoji) => (
<EmojiItem key={emoji.shortcode} emoji={emoji} />
))}
</EmojiGroup>
{packs.map((pack) => (
<EmojiGroup key={pack.id} id={pack.id} label={pack.meta.name || 'Unknown'}>
{pack
.getImages(ImageUsage.Emoticon)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode))
.map((image) => (
<CustomEmojiItem
key={image.shortcode}
mx={mx}
useAuthentication={useAuthentication}
image={image}
/>
))}
</EmojiGroup>
))}
{groups.map((emojiGroup) => (
<EmojiGroup key={emojiGroup.id} id={emojiGroup.id} label={labels[emojiGroup.id]}>
{emojiGroup.emojis.map((emoji) => (
<EmojiItem key={emoji.unicode} emoji={emoji} />
))}
</EmojiGroup>
))}
</>
);
}
const DefaultEmojiPreview: PreviewData = { key: '🙂', shortcode: 'slight_smile' };
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
matchOptions: {
contain: true,
},
};
type EmojiBoardProps = {
tab?: EmojiBoardTab;
onTabChange?: (tab: EmojiBoardTab) => void;
imagePackRooms: Room[];
requestClose: () => void;
returnFocusOnDeactivate?: boolean;
onEmojiSelect?: (unicode: string, shortcode: string) => void;
onCustomEmojiSelect?: (mxc: string, shortcode: string) => void;
onStickerSelect?: (mxc: string, shortcode: string, label: string) => void;
allowTextCustomEmoji?: boolean;
addToRecentEmoji?: boolean;
};
export function EmojiBoard({
tab = EmojiBoardTab.Emoji,
onTabChange,
imagePackRooms,
requestClose,
returnFocusOnDeactivate,
onEmojiSelect,
onCustomEmojiSelect,
onStickerSelect,
allowTextCustomEmoji,
addToRecentEmoji = true,
}: EmojiBoardProps) {
const emojiTab = tab === EmojiBoardTab.Emoji;
const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
const previewAtom = useMemo(
() => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined),
[emojiTab]
);
const setPreviewData = useSetAtom(previewAtom); const setPreviewData = useSetAtom(previewAtom);
const activeGroupIdAtom = useMemo(() => atom<string | undefined>(undefined), []);
const setActiveGroupId = useSetAtom(activeGroupIdAtom);
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const emojiGroupLabels = useEmojiGroupLabels();
const emojiGroupIcons = useEmojiGroupIcons();
const imagePacks = useRelevantImagePacks(usage, imagePackRooms);
const recentEmojis = useRecentEmoji(mx, 21);
const contentScrollRef = useRef<HTMLDivElement>(null);
const searchList = useMemo(() => {
let list: Array<PackImageReader | IEmoji> = [];
list = list.concat(imagePacks.flatMap((pack) => pack.getImages(usage)));
if (emojiTab) list = list.concat(emojis);
return list;
}, [emojiTab, usage, imagePacks]);
const [result, search, resetSearch] = useAsyncSearch(
searchList,
getEmoticonSearchStr,
SEARCH_OPTIONS
);
const searchedItems = result?.items.slice(0, 100);
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
useCallback(
(evt) => {
const term = evt.target.value;
if (term) search(term);
else resetSearch();
},
[search, resetSearch]
),
{ wait: 200 }
);
const syncActiveGroupId = useCallback(() => {
const targetEl = contentScrollRef.current;
if (!targetEl) return;
const groupEls = Array.from(targetEl.querySelectorAll('div[data-group-id]')) as HTMLElement[];
const groupEl = groupEls.find((el) => isIntersectingScrollView(targetEl, el));
const groupId = groupEl?.getAttribute('data-group-id') ?? undefined;
setActiveGroupId(groupId);
}, [setActiveGroupId]);
const handleOnScroll: UIEventHandler<HTMLDivElement> = useThrottle(syncActiveGroupId, {
wait: 500,
});
const handleScrollToGroup = (groupId: string) => {
setActiveGroupId(groupId);
const groupElement = document.getElementById(getDOMGroupId(groupId));
groupElement?.scrollIntoView();
};
const handleEmojiClick: MouseEventHandler = (evt) => {
const targetEl = targetFromEvent(evt.nativeEvent, 'button');
if (!targetEl) return;
const emojiInfo = getEmojiItemInfo(targetEl);
if (!emojiInfo) return;
if (emojiInfo.type === EmojiType.Emoji) {
onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
if (!evt.altKey && !evt.shiftKey) {
if (addToRecentEmoji) {
addRecentEmoji(mx, emojiInfo.data);
}
requestClose();
}
}
if (emojiInfo.type === EmojiType.CustomEmoji) {
onCustomEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
if (!evt.altKey && !evt.shiftKey) requestClose();
}
if (emojiInfo.type === EmojiType.Sticker) {
onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label);
if (!evt.altKey && !evt.shiftKey) requestClose();
}
};
const handleTextCustomEmojiSelect = (textEmoji: string) => {
onCustomEmojiSelect?.(textEmoji, textEmoji);
requestClose();
};
const handleEmojiPreview = useCallback( const handleEmojiPreview = useCallback(
(element: HTMLButtonElement) => { (element: HTMLButtonElement) => {
@ -446,13 +327,166 @@ export function EmojiBoard({
handleEmojiPreview(targetEl); handleEmojiPreview(targetEl);
}; };
// Reset scroll top on search and tab change return (
<Scroll ref={contentScrollRef} size="400" onKeyDown={preventScrollWithArrowKey} hideTrack>
<Box
onClick={onGroupItemClick}
onMouseMove={handleEmojiHover}
onFocus={handleEmojiFocus}
direction="Column"
>
{children}
</Box>
</Scroll>
);
}
const DefaultEmojiPreview: PreviewData = { key: '🙂', shortcode: 'slight_smile' };
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
matchOptions: {
contain: true,
},
};
const VIRTUAL_OVER_SCAN = 2;
type EmojiBoardProps = {
tab?: EmojiBoardTab;
onTabChange?: (tab: EmojiBoardTab) => void;
imagePackRooms: Room[];
requestClose: () => void;
returnFocusOnDeactivate?: boolean;
onEmojiSelect?: (unicode: string, shortcode: string) => void;
onCustomEmojiSelect?: (mxc: string, shortcode: string) => void;
onStickerSelect?: (mxc: string, shortcode: string, label: string) => void;
allowTextCustomEmoji?: boolean;
addToRecentEmoji?: boolean;
};
export function EmojiBoard({
tab = EmojiBoardTab.Emoji,
onTabChange,
imagePackRooms,
requestClose,
returnFocusOnDeactivate,
onEmojiSelect,
onCustomEmojiSelect,
onStickerSelect,
allowTextCustomEmoji,
addToRecentEmoji = true,
}: EmojiBoardProps) {
const mx = useMatrixClient();
const emojiTab = tab === EmojiBoardTab.Emoji;
const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
const previewAtom = useMemo(
() => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined),
[emojiTab]
);
const activeGroupIdAtom = useMemo(() => atom<string | undefined>(undefined), []);
const setActiveGroupId = useSetAtom(activeGroupIdAtom);
const imagePacks = useRelevantImagePacks(usage, imagePackRooms);
const [emojiGroupItems, stickerGroupItems] = useGroups(tab, imagePacks);
const groups = emojiTab ? emojiGroupItems : stickerGroupItems;
const renderItem = useItemRenderer(tab);
const searchList = useMemo(() => {
let list: Array<PackImageReader | IEmoji> = [];
list = list.concat(imagePacks.flatMap((pack) => pack.getImages(usage)));
if (emojiTab) list = list.concat(emojis);
return list;
}, [emojiTab, usage, imagePacks]);
const [result, search, resetSearch] = useAsyncSearch(
searchList,
getEmoticonSearchStr,
SEARCH_OPTIONS
);
const searchedItems = result?.items.slice(0, 100);
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
useCallback(
(evt) => {
const term = evt.target.value;
if (term) search(term);
else resetSearch();
},
[search, resetSearch]
),
{ wait: 200 }
);
const contentScrollRef = useRef<HTMLDivElement>(null);
const virtualBaseRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: groups.length,
getScrollElement: () => contentScrollRef.current,
estimateSize: () => 40,
overscan: VIRTUAL_OVER_SCAN,
});
const vItems = virtualizer.getVirtualItems();
const handleGroupItemClick: MouseEventHandler = (evt) => {
const targetEl = targetFromEvent(evt.nativeEvent, 'button');
const emojiInfo = targetEl && getEmojiItemInfo(targetEl);
if (!emojiInfo) return;
if (emojiInfo.type === EmojiType.Emoji) {
onEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
if (!evt.altKey && !evt.shiftKey && addToRecentEmoji) {
addRecentEmoji(mx, emojiInfo.data);
}
}
if (emojiInfo.type === EmojiType.CustomEmoji) {
onCustomEmojiSelect?.(emojiInfo.data, emojiInfo.shortcode);
}
if (emojiInfo.type === EmojiType.Sticker) {
onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label);
}
if (!evt.altKey && !evt.shiftKey) requestClose();
};
const handleTextCustomEmojiSelect = (textEmoji: string) => {
onCustomEmojiSelect?.(textEmoji, textEmoji);
requestClose();
};
const handleScrollToGroup = (groupId: string) => {
const groupIndex = groups.findIndex((group) => group.id === groupId);
virtualizer.scrollToIndex(groupIndex);
};
// sync active sidebar tab with scroll
useEffect(() => { useEffect(() => {
syncActiveGroupId(); const scrollElement = contentScrollRef.current;
contentScrollRef.current?.scrollTo({ if (scrollElement) {
top: 0, const scrollTop = scrollElement.offsetTop + scrollElement.scrollTop;
}); const offsetTop = virtualBaseRef.current?.offsetTop ?? 0;
}, [result, emojiTab, syncActiveGroupId]); const inViewVItem = vItems.find((vItem) => scrollTop < offsetTop + vItem.end);
const group = inViewVItem ? groups[inViewVItem?.index] : undefined;
setActiveGroupId(group?.id);
}
}, [vItems, groups, setActiveGroupId, result?.query]);
// reset scroll position on search
useEffect(() => {
const scrollElement = contentScrollRef.current;
if (scrollElement) {
scrollElement.scrollTo({ top: 0 });
}
}, [result?.query]);
// reset scroll position on tab change
useEffect(() => {
if (groups.length > 0) {
virtualizer.scrollToIndex(0);
}
}, [tab, virtualizer, groups]);
return ( return (
<FocusTrap <FocusTrap
@ -474,6 +508,7 @@ export function EmojiBoard({
<Box direction="Column" gap="200"> <Box direction="Column" gap="200">
{onTabChange && <EmojiBoardTabs tab={tab} onTabChange={onTabChange} />} {onTabChange && <EmojiBoardTabs tab={tab} onTabChange={onTabChange} />}
<SearchInput <SearchInput
key={tab}
query={result?.query} query={result?.query}
onChange={handleOnChange} onChange={handleOnChange}
allowTextCustomEmoji={allowTextCustomEmoji} allowTextCustomEmoji={allowTextCustomEmoji}
@ -485,58 +520,59 @@ export function EmojiBoard({
emojiTab ? ( emojiTab ? (
<EmojiSidebar <EmojiSidebar
activeGroupAtom={activeGroupIdAtom} activeGroupAtom={activeGroupIdAtom}
handleOpenGroup={handleScrollToGroup}
packs={imagePacks} packs={imagePacks}
groups={emojiGroups} onScrollToGroup={handleScrollToGroup}
icons={emojiGroupIcons}
labels={emojiGroupLabels}
/> />
) : ( ) : (
<StickerSidebar <StickerSidebar
activeGroupAtom={activeGroupIdAtom} activeGroupAtom={activeGroupIdAtom}
handleOpenGroup={handleScrollToGroup}
packs={imagePacks} packs={imagePacks}
onScrollToGroup={handleScrollToGroup}
/> />
) )
} }
> >
<Box grow="Yes"> <Box grow="Yes">
<Scroll <EmojiGroupHolder
ref={contentScrollRef} key={tab}
size="400" contentScrollRef={contentScrollRef}
onScroll={handleOnScroll} previewAtom={previewAtom}
onKeyDown={preventScrollWithArrowKey} onGroupItemClick={handleGroupItemClick}
hideTrack
> >
<Box {searchedItems && (
onClick={handleEmojiClick} <EmojiGroup
onMouseMove={handleEmojiHover} id={SEARCH_GROUP_ID}
onFocus={handleEmojiFocus} label={searchedItems.length ? 'Search Results' : 'No Results found'}
direction="Column" >
gap="200" {searchedItems.map(renderItem)}
</EmojiGroup>
)}
<div
ref={virtualBaseRef}
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
> >
{searchedItems && ( {vItems.map((vItem) => {
<SearchGroup const group = groups[vItem.index];
mx={mx}
tab={tab} return (
id={SEARCH_GROUP_ID} <VirtualTile
label={searchedItems.length ? 'Search Results' : 'No Results found'} virtualItem={vItem}
emojis={searchedItems} style={{ paddingTop: config.space.S200 }}
useAuthentication={useAuthentication} ref={virtualizer.measureElement}
/> key={vItem.index}
)} >
{emojiTab ? ( <EmojiGroup key={group.id} id={group.id} label={group.name}>
<EmojiGroups {group.items.map(renderItem)}
recentEmojis={recentEmojis} </EmojiGroup>
packs={imagePacks} </VirtualTile>
groups={emojiGroups} );
labels={emojiGroupLabels} })}
/> </div>
) : ( {tab === EmojiBoardTab.Sticker && groups.length === 0 && <NoStickerPacks />}
<StickerGroups packs={imagePacks} /> </EmojiGroupHolder>
)}
</Box>
</Scroll>
</Box> </Box>
<Preview previewAtom={previewAtom} /> <Preview previewAtom={previewAtom} />
</EmojiBoardLayout> </EmojiBoardLayout>