From 0b08e1de2c831c68da0dbbc7b5ecde7e4387a087 Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 11 Aug 2026 16:41:40 +0200 Subject: [PATCH] fix(app): keep emoji sprites aligned across zoom levels (#1557) --- .../features/channel/components/EmojiPicker.tsx | 14 -------------- .../channel/components/MobileEmojiPicker.tsx | 14 -------------- .../emoji_picker/EmojiPickerConstants.ts | 13 ++++++++++++- .../emoji_picker/EmojiPickerInspector.tsx | 14 ++++++-------- .../components/emoji_picker/EmojiRenderer.tsx | 14 ++++++-------- .../channel/components/emoji_picker/VirtualRow.tsx | 6 ------ 6 files changed, 24 insertions(+), 51 deletions(-) diff --git a/fluxer_app/src/features/channel/components/EmojiPicker.tsx b/fluxer_app/src/features/channel/components/EmojiPicker.tsx index d3eb8bde8..a736a08c0 100644 --- a/fluxer_app/src/features/channel/components/EmojiPicker.tsx +++ b/fluxer_app/src/features/channel/components/EmojiPicker.tsx @@ -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} diff --git a/fluxer_app/src/features/channel/components/MobileEmojiPicker.tsx b/fluxer_app/src/features/channel/components/MobileEmojiPicker.tsx index 1452f9f6b..a919e86eb 100644 --- a/fluxer_app/src/features/channel/components/MobileEmojiPicker.tsx +++ b/fluxer_app/src/features/channel/components/MobileEmojiPicker.tsx @@ -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} diff --git a/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerConstants.ts b/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerConstants.ts index 47370ba95..8e976563c 100644 --- a/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerConstants.ts +++ b/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerConstants.ts @@ -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/spritesheet-1f3fb@2x.png'; import sprite1f3fb1x from '@app/media/images/emoji-sprites/spritesheet-1f3fb.png'; import sprite1f3fc2x from '@app/media/images/emoji-sprites/spritesheet-1f3fc@2x.png'; @@ -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/spritesheet-emoji@2x.png'; 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; } diff --git a/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerInspector.tsx b/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerInspector.tsx index 44d78bdbc..145daca14 100644 --- a/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerInspector.tsx +++ b/fluxer_app/src/features/channel/components/emoji_picker/EmojiPickerInspector.tsx @@ -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, }; diff --git a/fluxer_app/src/features/channel/components/emoji_picker/EmojiRenderer.tsx b/fluxer_app/src/features/channel/components/emoji_picker/EmojiRenderer.tsx index 537ad79af..c43936574 100644 --- a/fluxer_app/src/features/channel/components/emoji_picker/EmojiRenderer.tsx +++ b/fluxer_app/src/features/channel/components/emoji_picker/EmojiRenderer.tsx @@ -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 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 = React.memo( handleHover, handleSelect, skinTone, - spriteSheetSizes, channel, shouldAnimate, gridColumns = 9, @@ -175,7 +173,6 @@ const VirtualRowRenderer: React.FC = 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 = observer( handleHover, handleSelect, skinTone, - spriteSheetSizes, channel, allowAnimation, gridColumns, @@ -287,7 +282,6 @@ export const VirtualizedRow: React.FC = observer( handleHover={handleHover} handleSelect={handleSelect} skinTone={skinTone} - spriteSheetSizes={spriteSheetSizes} channel={channel} shouldAnimate={allowAnimation && isInViewport} gridColumns={gridColumns}