mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
fix(app): reserve emoji picker cells while their image loads (#1920)
This commit is contained in:
@@ -19,7 +19,6 @@ import type {CSSProperties} from 'react';
|
||||
export const EMOJI_CLAP = '\u{1F44F}';
|
||||
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;
|
||||
const EMOJIS_PER_ROW_MIN = 5;
|
||||
|
||||
@@ -11,8 +11,8 @@ import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||
import {getEmojiDisplayDataWithSkinTone} from '@app/features/expressions/utils/SkinToneUtils';
|
||||
import UnicodeEmojis, {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
|
||||
import Guilds from '@app/features/guild/state/Guilds';
|
||||
import {getEmojiRenderUrl} from '@app/features/messaging/utils/markdown/EmojiDetector';
|
||||
import {isFirefoxBrowser} from '@app/features/ui/utils/NativeUtils';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
|
||||
@@ -22,7 +22,11 @@ interface EmojiPickerInspectorProps {
|
||||
|
||||
export const EmojiPickerInspector = observer(({hoveredEmoji}: EmojiPickerInspectorProps) => {
|
||||
const skinTone = Emoji.skinTone;
|
||||
const shouldAnimateEmoji = useShouldAnimate({kind: 'emoji', isHovering: Boolean(hoveredEmoji)});
|
||||
const shouldAnimateEmoji = useShouldAnimate({
|
||||
kind: 'emoji',
|
||||
isAnimated: Boolean(hoveredEmoji?.animated),
|
||||
isHovering: Boolean(hoveredEmoji),
|
||||
});
|
||||
const getEmojiForDisplay = (
|
||||
emoji: FlatEmoji | null,
|
||||
): {useImg: boolean; url?: string; style?: React.CSSProperties} | null => {
|
||||
@@ -30,7 +34,13 @@ export const EmojiPickerInspector = observer(({hoveredEmoji}: EmojiPickerInspect
|
||||
if (emoji.guildId || emoji.id) {
|
||||
return {
|
||||
url: emoji.id
|
||||
? AvatarUtils.getEmojiURL({id: emoji.id, animated: Boolean(emoji.animated) && shouldAnimateEmoji})
|
||||
? (getEmojiRenderUrl({
|
||||
id: emoji.id,
|
||||
surrogateUrl: null,
|
||||
isAnimatable: Boolean(emoji.animated),
|
||||
animated: shouldAnimateEmoji,
|
||||
jumbo: false,
|
||||
}) ?? '')
|
||||
: (emoji.url ?? ''),
|
||||
useImg: true,
|
||||
};
|
||||
@@ -46,9 +56,7 @@ export const EmojiPickerInspector = observer(({hoveredEmoji}: EmojiPickerInspect
|
||||
const index = hasSkinTones ? emoji.skinToneIndex : emoji.index;
|
||||
if (index === undefined) return {url: emoji.url, useImg: true};
|
||||
const perRow = hasSkinTones ? EMOJI_SPRITES.SkinTonePerRow : EMOJI_SPRITES.BasePerRow;
|
||||
const rows = Math.ceil(
|
||||
(hasSkinTones ? UnicodeEmojis.skinToneSpriteCount : UnicodeEmojis.baseSpriteCount) / perRow,
|
||||
);
|
||||
const rows = Math.ceil((hasSkinTones ? UnicodeEmojis.skinToneSpriteCount : UnicodeEmojis.baseSpriteCount) / perRow);
|
||||
return {
|
||||
style: {
|
||||
backgroundImage: getSpriteSheetBackground(hasSkinTones ? skinTone : ''),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import styles from '@app/features/channel/components/EmojiPicker.module.css';
|
||||
import {
|
||||
EMOJI_PICKER_CUSTOM_EMOJI_SIZE,
|
||||
getEmojiSpriteSheetLayout,
|
||||
getSpriteSheetBackground,
|
||||
} from '@app/features/channel/components/emoji_picker/EmojiPickerConstants';
|
||||
@@ -12,15 +11,45 @@ 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 UnicodeEmojis, {EMOJI_SPRITES} from '@app/features/expressions/utils/UnicodeEmojis';
|
||||
import {setUrlQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
|
||||
import {getEmojiRenderUrl} from '@app/features/messaging/utils/markdown/EmojiDetector';
|
||||
import {EmojiContextMenuItems} from '@app/features/ui/action_menu/items/EmojiContextMenuItems';
|
||||
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {isFirefoxBrowser} from '@app/features/ui/utils/NativeUtils';
|
||||
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
import React, {useEffect, useImperativeHandle, useRef} from 'react';
|
||||
import React, {useEffect, useImperativeHandle, useMemo, useRef} from 'react';
|
||||
|
||||
type PickerEmojiImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
const PickerEmojiImage = ({src, alt, ...props}: PickerEmojiImageProps) => {
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const hasLoadedRef = useRef(false);
|
||||
const handleLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const view = event.currentTarget?.ownerDocument?.defaultView ?? window;
|
||||
view.requestAnimationFrame(() => {
|
||||
if (imageRef.current == null) {
|
||||
return;
|
||||
}
|
||||
hasLoadedRef.current = true;
|
||||
imageRef.current.classList.remove(styles.emojiImageLoading);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<img
|
||||
data-flx="channel.emoji-picker.emoji-renderer.picker-emoji-image.emoji-image"
|
||||
{...props}
|
||||
ref={imageRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={hasLoadedRef.current ? styles.emojiImage : clsx(styles.emojiImage, styles.emojiImageLoading)}
|
||||
onLoad={hasLoadedRef.current ? undefined : handleLoad}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface EmojiRendererProps {
|
||||
emoji: FlatEmoji;
|
||||
@@ -57,11 +86,19 @@ export const EmojiRenderer = React.forwardRef<HTMLButtonElement, EmojiRendererPr
|
||||
}
|
||||
}, [shouldScrollIntoView]);
|
||||
const availability = checkEmojiAvailability(i18n, emoji, channel);
|
||||
const customEmojiUrl = emoji.id
|
||||
? setUrlQueryParams(AvatarUtils.getEmojiURL({id: emoji.id, animated: Boolean(emoji.animated) && shouldAnimate}), {
|
||||
size: EMOJI_PICKER_CUSTOM_EMOJI_SIZE,
|
||||
})
|
||||
: (emoji.url ?? '');
|
||||
const customEmojiUrl = useMemo(
|
||||
() =>
|
||||
emoji.id
|
||||
? (getEmojiRenderUrl({
|
||||
id: emoji.id,
|
||||
surrogateUrl: null,
|
||||
isAnimatable: Boolean(emoji.animated),
|
||||
animated: shouldAnimate,
|
||||
jumbo: false,
|
||||
}) ?? '')
|
||||
: (emoji.url ?? ''),
|
||||
[emoji.id, emoji.animated, emoji.url, shouldAnimate],
|
||||
);
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (!availability.canUse) {
|
||||
e.preventDefault();
|
||||
@@ -118,11 +155,9 @@ export const EmojiRenderer = React.forwardRef<HTMLButtonElement, EmojiRendererPr
|
||||
};
|
||||
if (emoji.guildId || emoji.id) {
|
||||
const content = (
|
||||
<img
|
||||
<PickerEmojiImage
|
||||
src={customEmojiUrl}
|
||||
alt={emoji.name}
|
||||
className={styles.emojiImage}
|
||||
loading="lazy"
|
||||
data-flx="channel.emoji-picker.emoji-renderer.emoji-image"
|
||||
/>
|
||||
);
|
||||
@@ -130,11 +165,9 @@ export const EmojiRenderer = React.forwardRef<HTMLButtonElement, EmojiRendererPr
|
||||
}
|
||||
if (!emoji.useSpriteSheet) {
|
||||
return renderButton(
|
||||
<img
|
||||
<PickerEmojiImage
|
||||
src={emoji.url ?? ''}
|
||||
alt={emoji.name}
|
||||
className={styles.emojiImage}
|
||||
loading="lazy"
|
||||
data-flx="channel.emoji-picker.emoji-renderer.emoji-image--2"
|
||||
/>,
|
||||
);
|
||||
@@ -145,31 +178,21 @@ export const EmojiRenderer = React.forwardRef<HTMLButtonElement, EmojiRendererPr
|
||||
const {url} = getEmojiDisplayDataWithSkinTone(emoji, skinTone);
|
||||
if (url) {
|
||||
return renderButton(
|
||||
<img
|
||||
src={url}
|
||||
alt={emoji.name}
|
||||
className={styles.emojiImage}
|
||||
loading="lazy"
|
||||
data-flx="channel.emoji-picker.emoji-renderer.emoji-image--4"
|
||||
/>,
|
||||
<PickerEmojiImage src={url} alt={emoji.name} data-flx="channel.emoji-picker.emoji-renderer.emoji-image--4" />,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (index === undefined) {
|
||||
return renderButton(
|
||||
<img
|
||||
<PickerEmojiImage
|
||||
src={emoji.url ?? ''}
|
||||
alt={emoji.name}
|
||||
className={styles.emojiImage}
|
||||
loading="lazy"
|
||||
data-flx="channel.emoji-picker.emoji-renderer.emoji-image--3"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
const perRow = hasSkinTones ? EMOJI_SPRITES.SkinTonePerRow : EMOJI_SPRITES.BasePerRow;
|
||||
const rows = Math.ceil(
|
||||
(hasSkinTones ? UnicodeEmojis.skinToneSpriteCount : UnicodeEmojis.baseSpriteCount) / perRow,
|
||||
);
|
||||
const rows = Math.ceil((hasSkinTones ? UnicodeEmojis.skinToneSpriteCount : UnicodeEmojis.baseSpriteCount) / perRow);
|
||||
const spriteStyle = {
|
||||
backgroundImage: getSpriteSheetBackground(hasSkinTones ? skinTone : ''),
|
||||
...getEmojiSpriteSheetLayout(index, perRow, rows),
|
||||
|
||||
Reference in New Issue
Block a user