Fix inversion of emoji colours based on dark/light mode (#37120)

This commit is contained in:
diondiondion
2025-12-05 11:08:59 +01:00
committed by GitHub
parent 7f1f3236c6
commit 591776d7ad
15 changed files with 86 additions and 30 deletions

View File

@@ -1,9 +1,14 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import classNames from 'classnames';
import { EMOJI_TYPE_CUSTOM } from '@/mastodon/features/emoji/constants'; import { EMOJI_TYPE_CUSTOM } from '@/mastodon/features/emoji/constants';
import { useEmojiAppState } from '@/mastodon/features/emoji/mode'; import { useEmojiAppState } from '@/mastodon/features/emoji/mode';
import { unicodeHexToUrl } from '@/mastodon/features/emoji/normalize'; import {
emojiToInversionClassName,
unicodeHexToUrl,
} from '@/mastodon/features/emoji/normalize';
import { import {
isStateLoaded, isStateLoaded,
loadEmojiDataToState, loadEmojiDataToState,
@@ -41,6 +46,9 @@ export const Emoji: FC<EmojiProps> = ({
}, [appState.currentLocale, state]); }, [appState.currentLocale, state]);
const animate = useContext(AnimateEmojiContext); const animate = useContext(AnimateEmojiContext);
const inversionClass = emojiToInversionClassName(code);
const fallback = showFallback ? code : null; const fallback = showFallback ? code : null;
// If the code is invalid or we otherwise know it's not valid, show the fallback. // If the code is invalid or we otherwise know it's not valid, show the fallback.
@@ -79,7 +87,7 @@ export const Emoji: FC<EmojiProps> = ({
src={src} src={src}
alt={state.data.unicode} alt={state.data.unicode}
title={state.data.label} title={state.data.label}
className='emojione' className={classNames('emojione', inversionClass)}
loading='lazy' loading='lazy'
/> />
); );

View File

@@ -116,3 +116,29 @@ export const EMOJIS_WITH_LIGHT_BORDER = [
'🪽', // 1FAE8 '🪽', // 1FAE8
'🪿', // 1FABF '🪿', // 1FABF
]; ];
export const EMOJIS_REQUIRING_INVERSION_IN_LIGHT_MODE = [
'⛓️', // 26D3-FE0F
];
export const EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE = [
'🔜', // 1F51C
'🔙', // 1F519
'🔛', // 1F51B
'🔝', // 1F51D
'🔚', // 1F51A
'©️', // 00A9 FE0F
'➰', // 27B0
'💱', // 1F4B1
'✔️', // 2714 FE0F
'➗', // 2797
'💲', // 1F4B2
'', // 2796
'✖️', // 2716 FE0F
'', // 2795
'®️', // 00AE FE0F
'🕷️', // 1F577 FE0F
'📞', // 1F4DE
'™️', // 2122 FE0F
'〰️', // 3030 FE0F
];

View File

@@ -1,5 +1,6 @@
import Trie from 'substring-trie'; import Trie from 'substring-trie';
import { getUserTheme, isDarkMode } from '@/mastodon/utils/theme';
import { assetHost } from 'mastodon/utils/config'; import { assetHost } from 'mastodon/utils/config';
import { autoPlayGif } from '../../initial_state'; import { autoPlayGif } from '../../initial_state';
@@ -97,9 +98,9 @@ const emojifyTextNode = (node, customEmojis) => {
const { filename, shortCode } = unicodeMapping[unicode_emoji]; const { filename, shortCode } = unicodeMapping[unicode_emoji];
const title = shortCode ? `:${shortCode}:` : ''; const title = shortCode ? `:${shortCode}:` : '';
const isSystemTheme = !!document.body?.classList.contains('theme-system'); const isSystemTheme = getUserTheme() === 'system';
const theme = (isSystemTheme || document.body?.classList.contains('theme-mastodon-light')) ? 'light' : 'dark'; const theme = (isSystemTheme || !isDarkMode()) ? 'light' : 'dark';
const imageFilename = emojiFilename(filename, theme); const imageFilename = emojiFilename(filename, theme);

View File

@@ -3,6 +3,7 @@
import { createAppSelector, useAppSelector } from '@/mastodon/store'; import { createAppSelector, useAppSelector } from '@/mastodon/store';
import { isDevelopment } from '@/mastodon/utils/environment'; import { isDevelopment } from '@/mastodon/utils/environment';
import { isDarkMode } from '@/mastodon/utils/theme';
import { import {
EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE,
@@ -27,7 +28,7 @@ export function useEmojiAppState(): EmojiAppState {
currentLocale: locale, currentLocale: locale,
locales: [locale], locales: [locale],
mode, mode,
darkTheme: document.body.classList.contains('theme-default'), darkTheme: isDarkMode(),
}; };
} }

View File

@@ -10,6 +10,8 @@ import {
SKIN_TONE_CODES, SKIN_TONE_CODES,
EMOJIS_WITH_DARK_BORDER, EMOJIS_WITH_DARK_BORDER,
EMOJIS_WITH_LIGHT_BORDER, EMOJIS_WITH_LIGHT_BORDER,
EMOJIS_REQUIRING_INVERSION_IN_LIGHT_MODE,
EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE,
} from './constants'; } from './constants';
import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types'; import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types';
@@ -150,6 +152,16 @@ export function twemojiToUnicodeInfo(
return hexNumbersToString(mappedCodes); return hexNumbersToString(mappedCodes);
} }
export function emojiToInversionClassName(emoji: string): string | null {
if (EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE.includes(emoji)) {
return 'invert-on-dark';
}
if (EMOJIS_REQUIRING_INVERSION_IN_LIGHT_MODE.includes(emoji)) {
return 'invert-on-light';
}
return null;
}
export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) { export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) {
if (!extraEmojis) { if (!extraEmojis) {
return null; return null;

View File

@@ -0,0 +1,13 @@
export function getUserTheme() {
const { userTheme } = document.documentElement.dataset;
return userTheme;
}
export function isDarkMode() {
const { userTheme } = document.documentElement.dataset;
return (
(userTheme === 'system' &&
window.matchMedia('(prefers-color-scheme: dark)').matches) ||
userTheme !== 'mastodon-light'
);
}

View File

@@ -1,2 +1 @@
@use 'mastodon/variables';
@use 'common'; @use 'common';

View File

@@ -1,3 +1,4 @@
@use 'mastodon/variables';
@use 'mastodon/mixins'; @use 'mastodon/mixins';
@use 'fonts/roboto'; @use 'fonts/roboto';
@use 'fonts/roboto-mono'; @use 'fonts/roboto-mono';
@@ -21,5 +22,4 @@
@use 'mastodon/admin'; @use 'mastodon/admin';
@use 'mastodon/dashboard'; @use 'mastodon/dashboard';
@use 'mastodon/rtl'; @use 'mastodon/rtl';
@use 'mastodon/accessibility';
@use 'mastodon/rich_text'; @use 'mastodon/rich_text';

View File

@@ -1,3 +1,2 @@
@use 'mastodon/variables';
@use 'common'; @use 'common';
@use 'mastodon/high-contrast'; @use 'mastodon/high-contrast';

View File

@@ -1,4 +1 @@
@use 'mastodon/variables' with (
$emojis-requiring-inversion: 'chains'
);
@use 'common'; @use 'common';

View File

@@ -20,8 +20,3 @@ $no-columns-breakpoint: 600px;
$font-sans-serif: 'mastodon-font-sans-serif' !default; $font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default; $font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default; $font-monospace: 'mastodon-font-monospace' !default;
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;

View File

@@ -1,13 +0,0 @@
@use 'variables' as *;
%emoji-color-inversion {
filter: invert(1);
}
.emojione {
@each $emoji in $emojis-requiring-inversion {
&[title=':#{$emoji}:'] {
@extend %emoji-color-inversion;
}
}
}

View File

@@ -1,3 +1,15 @@
@function css-alpha($base-color, $amount) { @function css-alpha($base-color, $amount) {
@return #{rgb(from $base-color r g b / $amount)}; @return #{rgb(from $base-color r g b / $amount)};
} }
@mixin invert-on-light {
.invert-on-light {
filter: invert(1);
}
}
@mixin invert-on-dark {
.invert-on-dark {
filter: invert(1);
}
}

View File

@@ -1,6 +1,7 @@
@use 'base'; @use 'base';
@use 'dark'; @use 'dark';
@use 'light'; @use 'light';
@use 'utils';
html { html {
@include base.palette; @include base.palette;
@@ -10,6 +11,7 @@ html {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@include dark.tokens; @include dark.tokens;
@include utils.invert-on-dark;
@media (prefers-contrast: more) { @media (prefers-contrast: more) {
@include dark.contrast-overrides; @include dark.contrast-overrides;
@@ -18,6 +20,7 @@ html {
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
@include light.tokens; @include light.tokens;
@include utils.invert-on-light;
@media (prefers-contrast: more) { @media (prefers-contrast: more) {
@include light.contrast-overrides; @include light.contrast-overrides;
@@ -33,6 +36,7 @@ html:where(
color-scheme: dark; color-scheme: dark;
@include dark.tokens; @include dark.tokens;
@include utils.invert-on-dark;
} }
html[data-user-theme='contrast'], html[data-user-theme='contrast'],
@@ -45,4 +49,5 @@ html:where([data-user-theme='mastodon-light']) {
color-scheme: light; color-scheme: light;
@include light.tokens; @include light.tokens;
@include utils.invert-on-light;
} }

View File

@@ -12,7 +12,8 @@ RSpec.describe ThemeHelper do
it 'returns the mastodon-light and application stylesheets with correct color schemes' do it 'returns the mastodon-light and application stylesheets with correct color schemes' do
expect(html_links.first.attributes.symbolize_keys) expect(html_links.first.attributes.symbolize_keys)
.to include( .to include(
href: have_attributes(value: match(/mastodon-light/)), # This is now identical to the default theme & will be unified very soon
href: have_attributes(value: match(/default/)),
media: have_attributes(value: 'not all and (prefers-color-scheme: dark)') media: have_attributes(value: 'not all and (prefers-color-scheme: dark)')
) )
expect(html_links.last.attributes.symbolize_keys) expect(html_links.last.attributes.symbolize_keys)