fix(app): keep emoji sprites aligned across zoom levels (#1557)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-11 16:41:40 +02:00
1 parent 06243c48d2
commit 0b08e1de2c
6 files changed
+24 -51

No files matched your search

@@ -5,7 +5,6 @@ import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAut
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
import styles from '@app/features/channel/components/EmojiPicker.module.css';
import {EmojiPickerCategoryList} from '@app/features/channel/components/emoji_picker/EmojiPickerCategoryList';
import {EMOJI_SPRITE_SIZE} from '@app/features/channel/components/emoji_picker/EmojiPickerConstants';
import {EmojiPickerInspector} from '@app/features/channel/components/emoji_picker/EmojiPickerInspector';
import {EmojiPickerSearchBar} from '@app/features/channel/components/emoji_picker/EmojiPickerSearchBar';
import {useEmojiCategories} from '@app/features/channel/components/emoji_picker/hooks/useEmojiCategories';
@@ -26,7 +25,6 @@ import {
shouldShowEmojiPremiumUpsell,
} from '@app/features/expressions/utils/ExpressionPermissionUtils';
import {getEmojiDisplayDataWithSkinTone} from '@app/features/expressions/utils/SkinToneUtils';
import UnicodeEmojis, {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
import Permission from '@app/features/permissions/state/Permission';
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
import {usePremiumUpsellData} from '@app/features/premium/hooks/usePremiumUpsellData';
@@ -107,17 +105,6 @@ export const EmojiPicker = observer(
},
[shouldAnimateEmoji, skinTone],
);
const spriteSheetSizes = useMemo(() => {
const nonDiversitySize = [
`${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.NonDiversityPerRow}px`,
`${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numNonDiversitySprites / EMOJI_SPRITES.NonDiversityPerRow)}px`,
].join(' ');
const diversitySize = [
`${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.DiversityPerRow}px`,
`${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numDiversitySprites / EMOJI_SPRITES.DiversityPerRow)}px`,
].join(' ');
return {nonDiversitySize, diversitySize};
}, []);
useEffect(() => {
const handleEmojiDataUpdated = () => {
setEmojiDataVersion((version) => version + 1);
@@ -324,7 +311,6 @@ export const EmojiPicker = observer(
handleHover={handleHover}
handleSelect={handleEmojiSelect}
skinTone={skinTone}
spriteSheetSizes={spriteSheetSizes}
channel={channel}
allowAnimation={shouldAnimateEmoji}
hoveredEmoji={hoveredEmoji}
@@ -4,7 +4,6 @@ import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstant
import {useSearchInputAutofocus} from '@app/features/app/hooks/useSearchInputAutofocus';
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
import {EmojiPickerCategoryList} from '@app/features/channel/components/emoji_picker/EmojiPickerCategoryList';
import {EMOJI_SPRITE_SIZE} from '@app/features/channel/components/emoji_picker/EmojiPickerConstants';
import {EmojiPickerSearchBar} from '@app/features/channel/components/emoji_picker/EmojiPickerSearchBar';
import {useEmojiCategories} from '@app/features/channel/components/emoji_picker/hooks/useEmojiCategories';
import {useVirtualRows} from '@app/features/channel/components/emoji_picker/hooks/useVirtualRows';
@@ -26,7 +25,6 @@ import {
shouldShowEmojiPremiumUpsell,
} from '@app/features/expressions/utils/ExpressionPermissionUtils';
import {getEmojiDisplayDataWithSkinTone} from '@app/features/expressions/utils/SkinToneUtils';
import UnicodeEmojis, {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
import Permission from '@app/features/permissions/state/Permission';
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
import {usePremiumUpsellData} from '@app/features/premium/hooks/usePremiumUpsellData';
@@ -80,17 +78,6 @@ export const MobileEmojiPicker = observer(
const getEmojiGuildId = useCallback((emoji: FlatEmoji) => emoji.guildId, []);
const skinTone = Emoji.skinTone;
const shouldAnimateEmoji = useShouldAnimate({kind: 'emoji'});
const spriteSheetSizes = useMemo(() => {
const nonDiversitySize = [
`${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.NonDiversityPerRow}px`,
`${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numNonDiversitySprites / EMOJI_SPRITES.NonDiversityPerRow)}px`,
].join(' ');
const diversitySize = [
`${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.DiversityPerRow}px`,
`${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numDiversitySprites / EMOJI_SPRITES.DiversityPerRow)}px`,
].join(' ');
return {nonDiversitySize, diversitySize};
}, []);
const searchTerm = externalSearchTerm ?? internalSearchTerm;
const setSearchTerm = externalSetSearchTerm ?? setInternalSearchTerm;
const normalizedSearchTerm = useMemo(() => normalizeEmojiSearchQuery(searchTerm), [searchTerm]);
@@ -296,7 +283,6 @@ export const MobileEmojiPicker = observer(
handleHover={handleHover}
handleSelect={handleSelect}
skinTone={skinTone}
spriteSheetSizes={spriteSheetSizes}
channel={channel}
allowAnimation={shouldAnimateEmoji}
gridColumns={gridColumns}
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {convertToCodePoints} from '@app/features/expressions/utils/EmojiCodepointUtils';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import sprite1f3fb2x from '@app/media/images/emoji-sprites/[email protected]';
import sprite1f3fb1x from '@app/media/images/emoji-sprites/spritesheet-1f3fb.png';
import sprite1f3fc2x from '@app/media/images/emoji-sprites/[email protected]';
@@ -13,15 +14,25 @@ import sprite1f3ff2x from '@app/media/images/emoji-sprites/spritesheet-1f3ff@2x.
import sprite1f3ff1x from '@app/media/images/emoji-sprites/spritesheet-1f3ff.png';
import spriteDefault2x from '@app/media/images/emoji-sprites/[email protected]';
import spriteDefault1x from '@app/media/images/emoji-sprites/spritesheet-emoji.png';
import type {CSSProperties} from 'react';
export const EMOJI_CLAP = '\u{1F44F}';
export const EMOJI_SPRITE_SIZE = 32;
const EMOJI_SPRITE_SIZE = 32;
export const EMOJI_ROW_HEIGHT = 48;
export const EMOJI_PICKER_CUSTOM_EMOJI_SIZE = 48;
export const CATEGORY_HEADER_HEIGHT = 32;
export const EMOJIS_PER_ROW = 9;
export const OVERSCAN_ROWS = 5;
export const getEmojiSpriteSheetLayout = (index: number, perRow: number, rows: number): CSSProperties => {
const column = index % perRow;
const row = Math.floor(index / perRow);
return {
backgroundPosition: `${remFromPx(-column * EMOJI_SPRITE_SIZE)} ${remFromPx(-row * EMOJI_SPRITE_SIZE)}`,
backgroundSize: `${remFromPx(perRow * EMOJI_SPRITE_SIZE)} ${remFromPx(rows * EMOJI_SPRITE_SIZE)}`,
};
};
interface SpriteSheetOptions {
retina?: boolean;
}
@@ -3,13 +3,13 @@
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
import styles from '@app/features/channel/components/EmojiPicker.module.css';
import {
EMOJI_SPRITE_SIZE,
getEmojiSpriteSheetLayout,
getSpriteSheetBackground,
} from '@app/features/channel/components/emoji_picker/EmojiPickerConstants';
import Emoji from '@app/features/emoji/state/Emoji';
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
import {getEmojiDisplayDataWithSkinTone} from '@app/features/expressions/utils/SkinToneUtils';
import {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
import UnicodeEmojis, {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
import Guilds from '@app/features/guild/state/Guilds';
import {isFirefoxBrowser} from '@app/features/ui/utils/NativeUtils';
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
@@ -46,15 +46,13 @@ export const EmojiPickerInspector = observer(({hoveredEmoji}: EmojiPickerInspect
const index = hasDiversity ? emoji.diversityIndex : emoji.index;
if (index === undefined) return {url: emoji.url, useImg: true};
const perRow = hasDiversity ? EMOJI_SPRITES.DiversityPerRow : EMOJI_SPRITES.NonDiversityPerRow;
const x = -(index % perRow) * EMOJI_SPRITE_SIZE;
const y = -Math.floor(index / perRow) * EMOJI_SPRITE_SIZE;
const rows = Math.ceil(
(hasDiversity ? UnicodeEmojis.numDiversitySprites : UnicodeEmojis.numNonDiversitySprites) / perRow,
);
return {
style: {
backgroundImage: getSpriteSheetBackground(hasDiversity ? skinTone : ''),
backgroundPosition: `${x}px ${y}px`,
backgroundSize: hasDiversity
? `${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.DiversityPerRow}px`
: `${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.NonDiversityPerRow}px`,
...getEmojiSpriteSheetLayout(index, perRow, rows),
},
useImg: false,
};
@@ -3,7 +3,7 @@
import styles from '@app/features/channel/components/EmojiPicker.module.css';
import {
EMOJI_PICKER_CUSTOM_EMOJI_SIZE,
EMOJI_SPRITE_SIZE,
getEmojiSpriteSheetLayout,
getSpriteSheetBackground,
} from '@app/features/channel/components/emoji_picker/EmojiPickerConstants';
import type {Channel} from '@app/features/channel/models/Channel';
@@ -11,7 +11,7 @@ import * as EmojiPickerCommands from '@app/features/emoji/commands/EmojiPickerCo
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
import {checkEmojiAvailability} from '@app/features/expressions/utils/ExpressionPermissionUtils';
import {getEmojiDisplayDataWithSkinTone} from '@app/features/expressions/utils/SkinToneUtils';
import {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
import UnicodeEmojis, {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
import {setUrlQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
import {EmojiContextMenuItems} from '@app/features/ui/action_menu/items/EmojiContextMenuItems';
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
@@ -27,7 +27,6 @@ interface EmojiRendererProps {
handleHover: (emoji: FlatEmoji | null) => void;
handleSelect: (emoji: FlatEmoji, shiftKey?: boolean) => void;
skinTone: string;
spriteSheetSizes: {nonDiversitySize: string; diversitySize: string};
channel: Channel | null;
shouldAnimate: boolean;
isHighlighted?: boolean;
@@ -41,7 +40,6 @@ export const EmojiRenderer = React.forwardRef<HTMLButtonElement, EmojiRendererPr
handleHover,
handleSelect,
skinTone,
spriteSheetSizes,
channel,
shouldAnimate,
isHighlighted = false,
@@ -169,12 +167,12 @@ export const EmojiRenderer = React.forwardRef<HTMLButtonElement, EmojiRendererPr
);
}
const perRow = hasDiversity ? EMOJI_SPRITES.DiversityPerRow : EMOJI_SPRITES.NonDiversityPerRow;
const x = -(index % perRow) * EMOJI_SPRITE_SIZE;
const y = -Math.floor(index / perRow) * EMOJI_SPRITE_SIZE;
const rows = Math.ceil(
(hasDiversity ? UnicodeEmojis.numDiversitySprites : UnicodeEmojis.numNonDiversitySprites) / perRow,
);
const spriteStyle = {
backgroundImage: getSpriteSheetBackground(hasDiversity ? skinTone : ''),
backgroundPosition: `${x}px ${y}px`,
backgroundSize: hasDiversity ? spriteSheetSizes.diversitySize : spriteSheetSizes.nonDiversitySize,
...getEmojiSpriteSheetLayout(index, perRow, rows),
};
return renderButton(
<div
@@ -28,7 +28,6 @@ interface VirtualRowRendererProps {
handleHover: (emoji: FlatEmoji | null, row?: number, column?: number) => void;
handleSelect: (emoji: FlatEmoji, shiftKey?: boolean) => void;
skinTone: string;
spriteSheetSizes: {nonDiversitySize: string; diversitySize: string};
channel: Channel | null;
shouldAnimate: boolean;
gridColumns?: number;
@@ -47,7 +46,6 @@ const VirtualRowRenderer: React.FC<VirtualRowRendererProps> = React.memo(
handleHover,
handleSelect,
skinTone,
spriteSheetSizes,
channel,
shouldAnimate,
gridColumns = 9,
@@ -175,7 +173,6 @@ const VirtualRowRenderer: React.FC<VirtualRowRendererProps> = React.memo(
handleHover={(e) => handleHover(e, emojiRowIndex, colIndex)}
handleSelect={handleSelect}
skinTone={skinTone}
spriteSheetSizes={spriteSheetSizes}
channel={channel}
shouldAnimate={shouldAnimate}
isHighlighted={shouldHighlight}
@@ -203,7 +200,6 @@ interface VirtualizedRowProps {
handleHover: (emoji: FlatEmoji | null, row?: number, column?: number) => void;
handleSelect: (emoji: FlatEmoji, shiftKey?: boolean) => void;
skinTone: string;
spriteSheetSizes: {nonDiversitySize: string; diversitySize: string};
channel: Channel | null;
allowAnimation: boolean;
gridColumns?: number;
@@ -222,7 +218,6 @@ export const VirtualizedRow: React.FC<VirtualizedRowProps> = observer(
handleHover,
handleSelect,
skinTone,
spriteSheetSizes,
channel,
allowAnimation,
gridColumns,
@@ -287,7 +282,6 @@ export const VirtualizedRow: React.FC<VirtualizedRowProps> = observer(
handleHover={handleHover}
handleSelect={handleSelect}
skinTone={skinTone}
spriteSheetSizes={spriteSheetSizes}
channel={channel}
shouldAnimate={allowAnimation && isInViewport}
gridColumns={gridColumns}