extract component from emoji/sticker item and sidebar buttons

This commit is contained in:
Ajay Bura 2025-09-13 14:25:01 +05:30
parent f674482911
commit 7ff18a3ed3
6 changed files with 262 additions and 232 deletions

View file

@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
import { DefaultReset, color, config, toRem } from 'folds';
export const Base = style({
maxWidth: toRem(432),
@ -46,46 +46,3 @@ export const EmojiGroupContent = style([
padding: `0 ${config.space.S200}`,
},
]);
export const EmojiItem = style([
DefaultReset,
FocusOutline,
{
width: toRem(48),
height: toRem(48),
fontSize: toRem(32),
lineHeight: toRem(32),
borderRadius: config.radii.R400,
cursor: 'pointer',
':hover': {
backgroundColor: color.Surface.ContainerHover,
},
},
]);
export const StickerItem = style([
EmojiItem,
{
width: toRem(112),
height: toRem(112),
},
]);
export const CustomEmojiImg = style([
DefaultReset,
{
width: toRem(32),
height: toRem(32),
objectFit: 'contain',
},
]);
export const StickerImg = style([
DefaultReset,
{
width: toRem(96),
height: toRem(96),
objectFit: 'contain',
},
]);

View file

@ -10,7 +10,7 @@ import React, {
useMemo,
useRef,
} from 'react';
import { Box, Icon, Icons, Line, Scroll, Text, as, toRem } from 'folds';
import { Box, Icons, Line, Scroll, Text, as } from 'folds';
import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey';
import classNames from 'classnames';
@ -39,12 +39,16 @@ import {
EmojiBoardTabs,
SidebarStack,
SidebarDivider,
SidebarBtn,
Sidebar,
NoStickerPacks,
createPreviewDataAtom,
Preview,
PreviewData,
EmojiItem,
StickerItem,
CustomEmojiItem,
ImageGroupIcon,
GroupIcon,
} from './components';
import { EmojiBoardTab, EmojiItemInfo, EmojiType } from './types';
@ -125,81 +129,18 @@ export const EmojiGroup = as<
</Box>
));
export function EmojiItem({
label,
type,
data,
shortcode,
children,
}: {
label: string;
type: EmojiType;
data: string;
shortcode: string;
children: ReactNode;
}) {
return (
<Box
as="button"
className={css.EmojiItem}
type="button"
alignItems="Center"
justifyContent="Center"
title={label}
aria-label={`${label} emoji`}
data-emoji-type={type}
data-emoji-data={data}
data-emoji-shortcode={shortcode}
>
{children}
</Box>
);
}
export function StickerItem({
label,
type,
data,
shortcode,
children,
}: {
label: string;
type: EmojiType;
data: string;
shortcode: string;
children: ReactNode;
}) {
return (
<Box
as="button"
className={css.StickerItem}
type="button"
alignItems="Center"
justifyContent="Center"
title={label}
aria-label={`${label} sticker`}
data-emoji-type={type}
data-emoji-data={data}
data-emoji-shortcode={shortcode}
>
{children}
</Box>
);
}
function RecentEmojiSidebarStack({ onItemClick }: { onItemClick: (id: string) => void }) {
const activeGroupId = useAtomValue(activeGroupIdAtom);
return (
<SidebarStack>
<SidebarBtn
<GroupIcon
active={activeGroupId === RECENT_GROUP_ID}
id={RECENT_GROUP_ID}
label="Recent"
onItemClick={() => onItemClick(RECENT_GROUP_ID)}
>
<Icon src={Icons.RecentClock} filled={activeGroupId === RECENT_GROUP_ID} />
</SidebarBtn>
icon={Icons.RecentClock}
onClick={onItemClick}
/>
</SidebarStack>
);
}
@ -224,27 +165,19 @@ function ImagePackSidebarStack({
{packs.map((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar;
return (
<SidebarBtn
active={activeGroupId === pack.id}
<ImageGroupIcon
key={pack.id}
active={activeGroupId === pack.id}
id={pack.id}
label={label || 'Unknown Pack'}
onItemClick={onItemClick}
>
<img
style={{
width: toRem(24),
height: toRem(24),
objectFit: 'contain',
}}
src={
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ||
pack.meta.avatar
}
alt={label || 'Unknown Pack'}
/>
</SidebarBtn>
label={label ?? 'Unknown Pack'}
url={url}
onClick={onItemClick}
/>
);
})}
</SidebarStack>
@ -267,15 +200,14 @@ function NativeEmojiSidebarStack({
<SidebarStack className={css.NativeEmojiSidebarStack}>
<SidebarDivider />
{groups.map((group) => (
<SidebarBtn
<GroupIcon
key={group.id}
active={activeGroupId === group.id}
id={group.id}
label={labels[group.id]}
onItemClick={onItemClick}
>
<Icon src={icons[group.id]} filled={activeGroupId === group.id} />
</SidebarBtn>
icon={icons[group.id]}
onClick={onItemClick}
/>
))}
</SidebarStack>
);
@ -293,15 +225,7 @@ export function RecentEmojiGroup({
return (
<EmojiGroup key={id} id={id} label={label}>
{recentEmojis.map((emoji) => (
<EmojiItem
key={emoji.unicode}
label={emoji.label}
type={EmojiType.Emoji}
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
</EmojiItem>
<EmojiItem key={emoji.shortcode} emoji={emoji} />
))}
</EmojiGroup>
);
@ -324,53 +248,29 @@ export function SearchEmojiGroup({
}) {
return (
<EmojiGroup key={id} id={id} label={label}>
{tab === EmojiBoardTab.Emoji
? searchResult.map((emoji) =>
'unicode' in emoji ? (
<EmojiItem
key={emoji.unicode}
label={emoji.label}
type={EmojiType.Emoji}
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
</EmojiItem>
) : (
<EmojiItem
key={emoji.shortcode}
label={emoji.body || emoji.shortcode}
type={EmojiType.CustomEmoji}
data={emoji.url}
shortcode={emoji.shortcode}
>
<img
loading="lazy"
className={css.CustomEmojiImg}
alt={emoji.body || emoji.shortcode}
src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/>
</EmojiItem>
)
)
: searchResult.map((emoji) =>
'unicode' in emoji ? null : (
<StickerItem
key={emoji.shortcode}
label={emoji.body || emoji.shortcode}
type={EmojiType.Sticker}
data={emoji.url}
shortcode={emoji.shortcode}
>
<img
loading="lazy"
className={css.StickerImg}
alt={emoji.body || emoji.shortcode}
src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/>
</StickerItem>
)
)}
{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>
);
}
@ -392,20 +292,12 @@ export const CustomEmojiGroups = memo(
.getImages(ImageUsage.Emoticon)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode))
.map((image) => (
<EmojiItem
<CustomEmojiItem
key={image.shortcode}
label={image.body || image.shortcode}
type={EmojiType.CustomEmoji}
data={image.url}
shortcode={image.shortcode}
>
<img
loading="lazy"
className={css.CustomEmojiImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/>
</EmojiItem>
mx={mx}
useAuthentication={useAuthentication}
image={image}
/>
))}
</EmojiGroup>
))}
@ -434,18 +326,10 @@ export const StickerGroups = memo(
.map((image) => (
<StickerItem
key={image.shortcode}
label={image.body || image.shortcode}
type={EmojiType.Sticker}
data={image.url}
shortcode={image.shortcode}
>
<img
loading="lazy"
className={css.StickerImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/>
</StickerItem>
mx={mx}
useAuthentication={useAuthentication}
image={image}
/>
))}
</EmojiGroup>
))
@ -458,15 +342,7 @@ export const NativeEmojiGroups = memo(
{groups.map((emojiGroup) => (
<EmojiGroup key={emojiGroup.id} id={emojiGroup.id} label={labels[emojiGroup.id]}>
{emojiGroup.emojis.map((emoji) => (
<EmojiItem
key={emoji.unicode}
label={emoji.label}
type={EmojiType.Emoji}
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
</EmojiItem>
<EmojiItem key={emoji.unicode} emoji={emoji} />
))}
</EmojiGroup>
))}

View file

@ -0,0 +1,89 @@
import React from 'react';
import { Box } from 'folds';
import { MatrixClient } from 'matrix-js-sdk';
import { EmojiType } from '../types';
import * as css from './styles.css';
import { PackImageReader } from '../../../plugins/custom-emoji';
import { IEmoji } from '../../../plugins/emoji';
import { mxcUrlToHttp } from '../../../utils/matrix';
type EmojiItemProps = {
emoji: IEmoji;
};
export function EmojiItem({ emoji }: EmojiItemProps) {
return (
<Box
as="button"
type="button"
alignItems="Center"
justifyContent="Center"
className={css.EmojiItem}
title={emoji.label}
aria-label={`${emoji.label} emoji`}
data-emoji-type={EmojiType.Emoji}
data-emoji-data={emoji.unicode}
data-emoji-shortcode={emoji.shortcode}
>
{emoji.unicode}
</Box>
);
}
type CustomEmojiItemProps = {
mx: MatrixClient;
useAuthentication?: boolean;
image: PackImageReader;
};
export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) {
return (
<Box
as="button"
type="button"
alignItems="Center"
justifyContent="Center"
className={css.EmojiItem}
title={image.body || image.shortcode}
aria-label={`${image.body || image.shortcode} emoji`}
data-emoji-type={EmojiType.CustomEmoji}
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}
>
<img
loading="lazy"
className={css.CustomEmojiImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/>
</Box>
);
}
type StickerItemProps = {
mx: MatrixClient;
useAuthentication?: boolean;
image: PackImageReader;
};
export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) {
return (
<Box
as="button"
type="button"
alignItems="Center"
justifyContent="Center"
className={css.StickerItem}
title={image.body || image.shortcode}
aria-label={`${image.body || image.shortcode} emoji`}
data-emoji-type={EmojiType.Sticker}
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}
>
<img
loading="lazy"
className={css.StickerImg}
alt={image.body || image.shortcode}
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
/>
</Box>
);
}

View file

@ -1,5 +1,17 @@
import React, { ReactNode } from 'react';
import { Box, Scroll, Line, as, TooltipProvider, Tooltip, Text, IconButton } from 'folds';
import {
Box,
Scroll,
Line,
as,
TooltipProvider,
Tooltip,
Text,
IconButton,
Icon,
IconSrc,
Icons,
} from 'folds';
import classNames from 'classnames';
import * as css from './styles.css';
@ -31,17 +43,17 @@ export function SidebarDivider() {
return <Line className={css.SidebarDivider} size="300" variant="Surface" />;
}
export function SidebarBtn<T extends string>({
function SidebarBtn<T extends string>({
active,
label,
id,
onItemClick,
onClick,
children,
}: {
active?: boolean;
label: string;
id: T;
onItemClick: (id: T) => void;
onClick: (id: T) => void;
children: ReactNode;
}) {
return (
@ -59,7 +71,7 @@ export function SidebarBtn<T extends string>({
aria-pressed={active}
aria-labelledby={`SidebarStackItem-${id}-label`}
ref={ref}
onClick={() => onItemClick(id)}
onClick={() => onClick(id)}
size="400"
radii="300"
variant="Surface"
@ -70,3 +82,49 @@ export function SidebarBtn<T extends string>({
</TooltipProvider>
);
}
type GroupIconProps<T extends string> = {
active: boolean;
id: T;
label: string;
icon: IconSrc;
onClick: (id: T) => void;
};
export function GroupIcon<T extends string>({
active,
id,
label,
icon,
onClick,
}: GroupIconProps<T>) {
return (
<SidebarBtn active={active} id={id} label={label} onClick={onClick}>
<Icon src={icon} filled={active} />
</SidebarBtn>
);
}
type ImageGroupIconProps<T extends string> = {
active: boolean;
id: T;
label: string;
url?: string;
onClick: (id: T) => void;
};
export function ImageGroupIcon<T extends string>({
active,
id,
label,
url,
onClick,
}: ImageGroupIconProps<T>) {
return (
<SidebarBtn active={active} id={id} label={label} onClick={onClick}>
{url ? (
<Icon src={Icons.Photo} filled={active} />
) : (
<img className={css.SidebarBtnImg} src={url} alt={label} />
)}
</SidebarBtn>
);
}

View file

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

View file

@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
import { toRem, color, config, DefaultReset } from 'folds';
import { toRem, color, config, DefaultReset, FocusOutline } from 'folds';
export const Sidebar = style({
width: toRem(54),
@ -21,6 +21,12 @@ export const SidebarDivider = style({
width: toRem(18),
});
export const SidebarBtnImg = style({
width: toRem(24),
height: toRem(24),
objectFit: 'contain',
});
export const Preview = style({
padding: config.space.S200,
margin: config.space.S300,
@ -49,3 +55,46 @@ export const PreviewImg = style([
objectFit: 'contain',
},
]);
export const EmojiItem = style([
DefaultReset,
FocusOutline,
{
width: toRem(48),
height: toRem(48),
fontSize: toRem(32),
lineHeight: toRem(32),
borderRadius: config.radii.R400,
cursor: 'pointer',
':hover': {
backgroundColor: color.Surface.ContainerHover,
},
},
]);
export const StickerItem = style([
EmojiItem,
{
width: toRem(112),
height: toRem(112),
},
]);
export const CustomEmojiImg = style([
DefaultReset,
{
width: toRem(32),
height: toRem(32),
objectFit: 'contain',
},
]);
export const StickerImg = style([
DefaultReset,
{
width: toRem(96),
height: toRem(96),
objectFit: 'contain',
},
]);