create emoji/sticker preview atom

This commit is contained in:
Ajay Bura 2025-09-13 09:41:34 +05:30
parent e6a726b8fa
commit f674482911
5 changed files with 124 additions and 110 deletions

View file

@ -24,17 +24,6 @@ export const Header = style({
paddingBottom: 0,
});
export const Footer = style({
padding: config.space.S200,
margin: config.space.S300,
marginTop: 0,
minHeight: toRem(40),
borderRadius: config.radii.R400,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
});
export const EmojiGroup = style({
padding: `${config.space.S300} 0`,
});
@ -58,16 +47,6 @@ export const EmojiGroupContent = style([
},
]);
export const EmojiPreview = style([
DefaultReset,
{
width: toRem(32),
height: toRem(32),
fontSize: toRem(32),
lineHeight: toRem(32),
},
]);
export const EmojiItem = style([
DefaultReset,
FocusOutline,

View file

@ -42,6 +42,9 @@ import {
SidebarBtn,
Sidebar,
NoStickerPacks,
createPreviewDataAtom,
Preview,
PreviewData,
} from './components';
import { EmojiBoardTab, EmojiItemInfo, EmojiType } from './types';
@ -68,35 +71,14 @@ const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
const activeGroupIdAtom = atom<string | undefined>(undefined);
function Header({ children }: { children: ReactNode }) {
return (
<Box className={css.Header} direction="Column" shrink="No">
{children}
</Box>
);
}
function Content({ children }: { children: ReactNode }) {
return <Box grow="Yes">{children}</Box>;
}
function Footer({ children }: { children: ReactNode }) {
return (
<Box shrink="No" className={css.Footer} gap="300" alignItems="Center">
{children}
</Box>
);
}
const EmojiBoardLayout = as<
'div',
{
header: ReactNode;
sidebar?: ReactNode;
footer?: ReactNode;
children: ReactNode;
}
>(({ className, header, sidebar, footer, children, ...props }, ref) => (
>(({ className, header, sidebar, children, ...props }, ref) => (
<Box
display="InlineFlex"
className={classNames(css.Base, className)}
@ -105,9 +87,10 @@ const EmojiBoardLayout = as<
ref={ref}
>
<Box direction="Column" grow="Yes">
{header}
<Box className={css.Header} direction="Column" shrink="No">
{header}
</Box>
{children}
{footer}
</Box>
<Line size="300" direction="Vertical" />
{sidebar}
@ -439,10 +422,11 @@ export const StickerGroups = memo(
mx: MatrixClient;
groups: ImagePack[];
useAuthentication?: boolean;
}) => (
<>
{groups.length === 0 && <NoStickerPacks />}
{groups.map((pack) => (
}) =>
groups.length === 0 ? (
<NoStickerPacks />
) : (
groups.map((pack) => (
<EmojiGroup key={pack.id} id={pack.id} label={pack.meta.name || 'Unknown'}>
{pack
.getImages(ImageUsage.Sticker)
@ -464,9 +448,8 @@ export const StickerGroups = memo(
</StickerItem>
))}
</EmojiGroup>
))}
</>
)
))
)
);
export const NativeEmojiGroups = memo(
@ -491,6 +474,8 @@ export const NativeEmojiGroups = memo(
)
);
const DefaultEmojiPreview: PreviewData = { key: '🙂', shortcode: 'slight_smile' };
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
matchOptions: {
@ -525,6 +510,11 @@ export function EmojiBoard({
const stickerTab = tab === EmojiBoardTab.Sticker;
const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
const previewAtom = useMemo(
() => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined),
[emojiTab]
);
const setPreviewData = useSetAtom(previewAtom);
const setActiveGroupId = useSetAtom(activeGroupIdAtom);
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@ -534,8 +524,6 @@ export function EmojiBoard({
const recentEmojis = useRecentEmoji(mx, 21);
const contentScrollRef = useRef<HTMLDivElement>(null);
const emojiPreviewRef = useRef<HTMLDivElement>(null);
const emojiPreviewTextRef = useRef<HTMLParagraphElement>(null);
const searchList = useMemo(() => {
let list: Array<PackImageReader | IEmoji> = [];
@ -615,23 +603,14 @@ export function EmojiBoard({
const handleEmojiPreview = useCallback(
(element: HTMLButtonElement) => {
const emojiInfo = getEmojiItemInfo(element);
if (!emojiInfo || !emojiPreviewTextRef.current) return;
if (emojiInfo.type === EmojiType.Emoji && emojiPreviewRef.current) {
emojiPreviewRef.current.textContent = emojiInfo.data;
} else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) {
const img = document.createElement('img');
img.className = css.CustomEmojiImg;
img.setAttribute(
'src',
mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data
);
img.setAttribute('alt', emojiInfo.shortcode);
emojiPreviewRef.current.textContent = '';
emojiPreviewRef.current.appendChild(img);
}
emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`;
if (!emojiInfo) return;
setPreviewData({
key: emojiInfo.data,
shortcode: emojiInfo.shortcode,
});
},
[mx, useAuthentication]
[setPreviewData]
);
const throttleEmojiHover = useThrottle(handleEmojiPreview, {
@ -675,17 +654,15 @@ export function EmojiBoard({
>
<EmojiBoardLayout
header={
<Header>
<Box direction="Column" gap="200">
{onTabChange && <EmojiBoardTabs tab={tab} onTabChange={onTabChange} />}
<SearchInput
query={result?.query}
onChange={handleOnChange}
allowTextCustomEmoji={allowTextCustomEmoji}
onTextCustomEmojiSelect={handleTextCustomEmojiSelect}
/>
</Box>
</Header>
<Box direction="Column" gap="200">
{onTabChange && <EmojiBoardTabs tab={tab} onTabChange={onTabChange} />}
<SearchInput
query={result?.query}
onChange={handleOnChange}
allowTextCustomEmoji={allowTextCustomEmoji}
onTextCustomEmojiSelect={handleTextCustomEmojiSelect}
/>
</Box>
}
sidebar={
<Sidebar>
@ -711,34 +688,8 @@ export function EmojiBoard({
)}
</Sidebar>
}
footer={
emojiTab ? (
<Footer>
<Box
display="InlineFlex"
ref={emojiPreviewRef}
className={css.EmojiPreview}
alignItems="Center"
justifyContent="Center"
>
😃
</Box>
<Text ref={emojiPreviewTextRef} size="H5" truncate>
:smiley:
</Text>
</Footer>
) : (
imagePacks.length > 0 && (
<Footer>
<Text ref={emojiPreviewTextRef} size="H5" truncate>
:smiley:
</Text>
</Footer>
)
)
}
>
<Content>
<Box grow="Yes">
<Scroll
ref={contentScrollRef}
size="400"
@ -779,7 +730,8 @@ export function EmojiBoard({
{emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />}
</Box>
</Scroll>
</Content>
</Box>
<Preview previewAtom={previewAtom} />
</EmojiBoardLayout>
</FocusTrap>
);

View file

@ -0,0 +1,53 @@
import { Box, Text } from 'folds';
import React from 'react';
import { Atom, atom, useAtomValue } from 'jotai';
import * as css from './styles.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { mxcUrlToHttp } from '../../../utils/matrix';
export type PreviewData = {
key: string;
shortcode: string;
};
export const createPreviewDataAtom = (initial?: PreviewData) =>
atom<PreviewData | undefined>(initial);
type PreviewProps = {
previewAtom: Atom<PreviewData | undefined>;
};
export function Preview({ previewAtom }: PreviewProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { key, shortcode } = useAtomValue(previewAtom) ?? {};
if (!shortcode) return null;
return (
<Box shrink="No" className={css.Preview} gap="300" alignItems="Center">
{key && (
<Box
display="InlineFlex"
className={css.PreviewEmoji}
alignItems="Center"
justifyContent="Center"
>
{key.startsWith('mxc://') ? (
<img
className={css.PreviewImg}
src={mxcUrlToHttp(mx, key, useAuthentication) ?? key}
alt={shortcode}
/>
) : (
key
)}
</Box>
)}
<Text size="H5" truncate>
:{shortcode}:
</Text>
</Box>
);
}

View file

@ -2,3 +2,4 @@ export * from './SearchInput';
export * from './Tabs';
export * from './Sidebar';
export * from './NoStickerPacks';
export * from './Preview';

View file

@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
import { toRem, color, config } from 'folds';
import { toRem, color, config, DefaultReset } from 'folds';
export const Sidebar = style({
width: toRem(54),
@ -20,3 +20,32 @@ export const SidebarStack = style({
export const SidebarDivider = style({
width: toRem(18),
});
export const Preview = style({
padding: config.space.S200,
margin: config.space.S300,
marginTop: 0,
minHeight: toRem(40),
borderRadius: config.radii.R400,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
});
export const PreviewEmoji = style([
DefaultReset,
{
width: toRem(32),
height: toRem(32),
fontSize: toRem(32),
lineHeight: toRem(32),
},
]);
export const PreviewImg = style([
DefaultReset,
{
width: toRem(32),
height: toRem(32),
objectFit: 'contain',
},
]);