remove usage of emojify

This commit is contained in:
ChaosExAnima 2026-05-07 17:30:53 +02:00
parent 86e4ecfa20
commit 3c17fde5ef
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
6 changed files with 166 additions and 22 deletions

View File

@ -13,14 +13,17 @@ import axios from 'axios';
import { on } from 'delegated-events';
import { throttle } from 'lodash';
import { determineEmojiMode } from '@/mastodon/features/emoji/mode';
import { updateHtmlWithEmoji } from '@/mastodon/features/emoji/render';
import loadKeyboardExtensions from '@/mastodon/load_keyboard_extensions';
import { loadLocale, getLocale } from '@/mastodon/locales';
import { loadPolyfills } from '@/mastodon/polyfills';
import ready from '@/mastodon/ready';
import { assetHost } from '@/mastodon/utils/config';
import { isRecord } from '@/mastodon/utils/objects';
import { isDarkMode } from '@/mastodon/utils/theme';
import { formatTime } from '@/mastodon/utils/time';
import emojify from '../mastodon/features/emoji/emoji';
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
import { loadLocale, getLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
import 'cocoon-js-vanilla';
const messages = defineMessages({
@ -38,7 +41,7 @@ const messages = defineMessages({
},
});
function loaded() {
async function loaded() {
const { messages: localeData } = getLocale();
const locale = document.documentElement.lang;
@ -75,9 +78,32 @@ function loaded() {
return messageFormat.format(values) as string;
};
document.querySelectorAll('.emojify').forEach((content) => {
content.innerHTML = emojify(content.innerHTML);
});
let emojiStyle = 'auto';
const initialStateText =
document.getElementById('initial-state')?.textContent;
if (initialStateText) {
const state = JSON.parse(initialStateText) as unknown;
if (
isRecord(state) &&
'meta' in state &&
isRecord(state.meta) &&
'emoji_style' in state.meta &&
typeof state.meta.emoji_style === 'string'
) {
emojiStyle = state.meta.emoji_style;
}
}
const emojiMode = determineEmojiMode(emojiStyle);
const darkTheme = isDarkMode();
for (const element of document.querySelectorAll('.emojify')) {
await updateHtmlWithEmoji({
assetHost,
element,
locale,
mode: emojiMode,
darkTheme,
});
}
document
.querySelectorAll<HTMLTimeElement>('time.formatted')

View File

@ -34,6 +34,43 @@ export function useEmojiAppState(): EmojiAppState {
};
}
export function getEmojiAppState(): EmojiAppState {
const currentLocale = toSupportedLocale(document.documentElement.lang);
let emojiStyle = 'auto';
const initialStateText =
document.getElementById('initial-state')?.textContent;
if (initialStateText) {
try {
const state = JSON.parse(initialStateText) as unknown;
if (
state !== null &&
typeof state === 'object' &&
'meta' in state &&
state.meta !== null &&
typeof state.meta === 'object' &&
'emoji_style' in state.meta &&
typeof state.meta.emoji_style === 'string'
) {
emojiStyle = state.meta.emoji_style;
}
} catch (err: unknown) {
console.warn(
'Failed to parse initial state for emoji, defaulting to auto. Error:',
err,
);
}
}
return {
currentLocale,
locales: [currentLocale],
mode: determineEmojiMode(emojiStyle),
darkTheme: isDarkMode(),
assetHost,
};
}
type Feature = Uint8ClampedArray;
// See: https://github.com/nolanlawson/emoji-picker-element/blob/master/src/picker/constants.js

View File

@ -152,11 +152,11 @@ const CODES_WITH_LIGHT_BORDER = EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex);
export function unicodeHexToUrl({
unicodeHex,
darkTheme,
darkTheme = true,
assetHost,
}: {
unicodeHex: string;
darkTheme: boolean;
darkTheme?: boolean;
assetHost: string;
}): string {
const normalizedHex = unicodeToTwemojiHex(unicodeHex);

View File

@ -137,6 +137,18 @@ describe('loadEmojiDataToState', () => {
});
});
test('converts unicode emoji code to hexcode when loading data', async () => {
const dbCall = vi
.spyOn(db, 'loadEmojiByHexcode')
.mockResolvedValue(unicodeEmojiFactory());
const unicodeState = {
type: 'unicode',
code: '😊',
} as const satisfies EmojiStateUnicode;
await loadEmojiDataToState(unicodeState, 'en');
expect(dbCall).toHaveBeenCalledWith('1F60A', 'en');
});
test('returns null for custom emoji without data', async () => {
const customState = {
type: 'custom',

View File

@ -4,7 +4,9 @@ import {
EMOJI_TYPE_UNICODE,
EMOJI_TYPE_CUSTOM,
} from './constants';
import { emojiToInversionClassName, unicodeHexToUrl } from './normalize';
import type {
EmojiAppState,
EmojiLoadedState,
EmojiMode,
EmojiState,
@ -47,14 +49,14 @@ export function tokenizeText(text: string): TokenizedText {
if (code.startsWith(':') && code.endsWith(':')) {
// Custom emoji
tokens.push({
type: EMOJI_TYPE_CUSTOM,
code,
type: EMOJI_TYPE_CUSTOM,
} satisfies EmojiStateCustom);
} else {
// Unicode emoji
tokens.push({
code,
type: EMOJI_TYPE_UNICODE,
code: code,
} satisfies EmojiStateUnicode);
}
lastIndex = match.index + code.length;
@ -76,8 +78,8 @@ export function stringToEmojiState(
): EmojiStateUnicode | Required<EmojiStateCustom> | null {
if (isUnicodeEmoji(code)) {
return {
type: EMOJI_TYPE_UNICODE,
code: emojiToUnicodeHex(code),
type: EMOJI_TYPE_UNICODE,
};
}
@ -95,6 +97,65 @@ export function stringToEmojiState(
return null;
}
export async function updateHtmlWithEmoji({
assetHost,
darkTheme,
element,
mode,
locale,
}: {
element: Element;
locale: string;
} & Omit<EmojiAppState, 'currentLocale' | 'locales'>) {
if (mode === EMOJI_MODE_NATIVE) {
return;
}
const tokens = tokenizeText(element.innerHTML);
const newChildren: (string | Element)[] = [];
for (const token of tokens) {
if (typeof token === 'string') {
newChildren.push(token);
continue;
}
const state = await loadEmojiDataToState(token, locale);
// Ignore custom emoji if we encounter them.
if (!state || state.type === EMOJI_TYPE_CUSTOM) {
newChildren.push(token.code);
continue;
}
if (!shouldRenderImage(state, mode)) {
newChildren.push(state.data.unicode);
continue;
}
const img = document.createElement('img');
img.src = unicodeHexToUrl({
assetHost,
darkTheme,
unicodeHex: state.data.hexcode,
});
img.alt = state.data.unicode;
img.title = state.data.label;
img.classList.add('emojione');
const inversionClass = emojiToInversionClassName(state.data.unicode);
if (inversionClass) {
img.classList.add(inversionClass);
}
newChildren.push(img);
}
element.innerHTML = newChildren.reduce<string>(
(prev, curr) =>
typeof curr === 'string' ? prev + curr : prev + curr.outerHTML,
'',
);
}
/**
* Loads emoji data into the given state if not already loaded.
* @param state Emoji state to load data for.
@ -121,17 +182,19 @@ export async function loadEmojiDataToState(
LocaleNotLoadedError,
} = await import('./database');
const code = isUnicodeEmoji(state.code)
? emojiToUnicodeHex(state.code)
: state.code;
// First, try to load the data from IndexedDB.
try {
const legacyCode = await loadLegacyShortcodesByShortcode(state.code);
const legacyCode = await loadLegacyShortcodesByShortcode(code);
// This is duplicative, but that's because TS can't distinguish the state type easily.
const data = await loadEmojiByHexcode(
legacyCode?.hexcode ?? state.code,
locale,
);
const data = await loadEmojiByHexcode(legacyCode?.hexcode ?? code, locale);
if (data) {
return {
...state,
code,
type: EMOJI_TYPE_UNICODE,
data,
// TODO: Use CLDR shortcodes when the picker supports them.
@ -140,14 +203,14 @@ export async function loadEmojiDataToState(
}
// If not found, assume it's not an emoji and return null.
log('Could not find emoji %s for locale %s', state.code, locale);
log('Could not find emoji %s for locale %s', code, locale);
return null;
} catch (err: unknown) {
// If the locale is not loaded, load it and retry once.
if (!retry && err instanceof LocaleNotLoadedError) {
log(
'Error loading emoji %s for locale %s, loading locale and retrying.',
state.code,
code,
locale,
);
const { importEmojiData } = await import('./loader');

View File

@ -0,0 +1,6 @@
export type ValidObjectKey = string | number | symbol;
export type RecordObject = Record<ValidObjectKey, unknown>;
export function isRecord(value: unknown): value is RecordObject {
return typeof value === 'object' && value !== null;
}