From 02ea16150647ac3baf0bb8a89203ccc7200b4a2f Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Tue, 26 Mar 2024 10:25:49 +0100 Subject: [PATCH] Support "system" theme setting (light/dark theme depending on user system preference) (#29748) Co-authored-by: Nishiki Liu --- app/helpers/application_helper.rb | 9 +++++ .../features/emoji/__tests__/emoji-test.js | 38 +++++++++--------- .../mastodon/features/emoji/emoji.js | 39 +++++++++++++++---- app/lib/themes.rb | 2 +- app/views/layouts/application.html.haml | 2 +- app/views/layouts/embedded.html.haml | 2 +- app/views/layouts/error.html.haml | 2 +- config/locales/en.yml | 1 + config/settings.yml | 2 +- 9 files changed, 65 insertions(+), 32 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 668afe7fde1..d46d0674a48 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -160,6 +160,15 @@ module ApplicationHelper output.compact_blank.join(' ') end + def theme_style_tags(theme) + if theme == 'system' + concat stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') + concat stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') + else + stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous' + end + end + def cdn_host Rails.configuration.action_controller.asset_host end diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 7b917ac43b8..9d6ff5226a9 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -22,23 +22,23 @@ describe('emoji', () => { it('does unicode', () => { expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual( - '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).toEqual( - '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); - expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); + expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); expect(emojify('\u2757')).toEqual( - 'โ—'); + 'โ—'); }); it('does multiple unicode', () => { expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual( - 'โ— #๏ธโƒฃ'); + 'โ— #๏ธโƒฃ'); expect(emojify('\u2757#\uFE0F\u20E3')).toEqual( - 'โ—#๏ธโƒฃ'); + 'โ—#๏ธโƒฃ'); expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual( - 'โ— #๏ธโƒฃ โ—'); + 'โ— #๏ธโƒฃ โ—'); expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual( - 'foo โ— #๏ธโƒฃ bar'); + 'foo โ— #๏ธโƒฃ bar'); }); it('ignores unicode inside of tags', () => { @@ -46,16 +46,16 @@ describe('emoji', () => { }); it('does multiple emoji properly (issue 5188)', () => { - expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); - expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); }); it('does an emoji that has no shortcode', () => { - expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); + expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); }); it('does an emoji whose filename is irregular', () => { - expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); + expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); }); it('avoid emojifying on invisible text', () => { @@ -67,11 +67,11 @@ describe('emoji', () => { it('avoid emojifying on invisible text with nested tags', () => { expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); }); it('does not emojify emojis with textual presentation VS15 character', () => { @@ -79,19 +79,19 @@ describe('emoji', () => { .toEqual('โœด๏ธŽ'); }); - it('does an simple emoji properly', () => { + it('does a simple emoji properly', () => { expect(emojify('โ™€โ™‚')) - .toEqual('โ™€โ™‚'); + .toEqual('โ™€โ™‚'); }); it('does an emoji containing ZWJ properly', () => { expect(emojify('๐Ÿ’‚โ€โ™€๏ธ๐Ÿ’‚โ€โ™‚๏ธ')) - .toEqual('๐Ÿ’‚\u200Dโ™€๏ธ๐Ÿ’‚\u200Dโ™‚๏ธ'); + .toEqual('๐Ÿ’‚\u200Dโ™€๏ธ๐Ÿ’‚\u200Dโ™‚๏ธ'); }); it('keeps ordering as expected (issue fixed by PR 20677)', () => { expect(emojify('

๐Ÿ’• #foo test: foo.

')) - .toEqual('

๐Ÿ’• #foo test: foo.

'); + .toEqual('

๐Ÿ’• #foo test: foo.

'); }); }); }); diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 5918a65ed7d..e4aad302f65 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -17,8 +17,13 @@ const emojiFilenames = (emojis) => { const darkEmoji = emojiFilenames(['๐ŸŽฑ', '๐Ÿœ', 'โšซ', '๐Ÿ–ค', 'โฌ›', 'โ—ผ๏ธ', 'โ—พ', 'โ—ผ๏ธ', 'โœ’๏ธ', 'โ–ช๏ธ', '๐Ÿ’ฃ', '๐ŸŽณ', '๐Ÿ“ท', '๐Ÿ“ธ', 'โ™ฃ๏ธ', '๐Ÿ•ถ๏ธ', 'โœด๏ธ', '๐Ÿ”Œ', '๐Ÿ’‚โ€โ™€๏ธ', '๐Ÿ“ฝ๏ธ', '๐Ÿณ', '๐Ÿฆ', '๐Ÿ’‚', '๐Ÿ”ช', '๐Ÿ•ณ๏ธ', '๐Ÿ•น๏ธ', '๐Ÿ•‹', '๐Ÿ–Š๏ธ', '๐Ÿ–‹๏ธ', '๐Ÿ’‚โ€โ™‚๏ธ', '๐ŸŽค', '๐ŸŽ“', '๐ŸŽฅ', '๐ŸŽผ', 'โ™ ๏ธ', '๐ŸŽฉ', '๐Ÿฆƒ', '๐Ÿ“ผ', '๐Ÿ“น', '๐ŸŽฎ', '๐Ÿƒ', '๐Ÿด', '๐Ÿž', '๐Ÿ•บ', '๐Ÿ“ฑ', '๐Ÿ“ฒ', '๐Ÿšฒ', '๐Ÿชฎ', '๐Ÿฆโ€โฌ›']); const lightEmoji = emojiFilenames(['๐Ÿ‘ฝ', 'โšพ', '๐Ÿ”', 'โ˜๏ธ', '๐Ÿ’จ', '๐Ÿ•Š๏ธ', '๐Ÿ‘€', '๐Ÿฅ', '๐Ÿ‘ป', '๐Ÿ', 'โ•', 'โ”', 'โ›ธ๏ธ', '๐ŸŒฉ๏ธ', '๐Ÿ”Š', '๐Ÿ”‡', '๐Ÿ“ƒ', '๐ŸŒง๏ธ', '๐Ÿ', '๐Ÿš', '๐Ÿ™', '๐Ÿ“', '๐Ÿ‘', '๐Ÿ’€', 'โ˜ ๏ธ', '๐ŸŒจ๏ธ', '๐Ÿ”‰', '๐Ÿ”ˆ', '๐Ÿ’ฌ', '๐Ÿ’ญ', '๐Ÿ', '๐Ÿณ๏ธ', 'โšช', 'โฌœ', 'โ—ฝ', 'โ—ป๏ธ', 'โ–ซ๏ธ', '๐Ÿชฝ', '๐Ÿชฟ']); -const emojiFilename = (filename) => { - const borderedEmoji = (document.body && document.body.classList.contains('theme-mastodon-light')) ? lightEmoji : darkEmoji; +/** + * @param {string} filename + * @param {"light" | "dark" } colorScheme + * @returns {string} + */ +const emojiFilename = (filename, colorScheme) => { + const borderedEmoji = colorScheme === "light" ? lightEmoji : darkEmoji; return borderedEmoji.includes(filename) ? (filename + '_border') : filename; }; @@ -92,12 +97,30 @@ const emojifyTextNode = (node, customEmojis) => { const { filename, shortCode } = unicodeMapping[unicode_emoji]; const title = shortCode ? `:${shortCode}:` : ''; - replacement = document.createElement('img'); - replacement.setAttribute('draggable', 'false'); - replacement.setAttribute('class', 'emojione'); - replacement.setAttribute('alt', unicode_emoji); - replacement.setAttribute('title', title); - replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`); + replacement = document.createElement('picture'); + + const isSystemTheme = !!document.body?.classList.contains('theme-system'); + + if(isSystemTheme) { + let source = document.createElement('source'); + source.setAttribute('media', '(prefers-color-scheme: dark)'); + source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`); + replacement.appendChild(source); + } + + let img = document.createElement('img'); + img.setAttribute('draggable', 'false'); + img.setAttribute('class', 'emojione'); + img.setAttribute('alt', unicode_emoji); + img.setAttribute('title', title); + + let theme = "light"; + + if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light')) + theme = "dark"; + + img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`); + replacement.appendChild(img); } // Add the processed-up-to-now string and the emoji replacement diff --git a/app/lib/themes.rb b/app/lib/themes.rb index 243ffb9ab97..4010d84435c 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -11,6 +11,6 @@ class Themes end def names - @conf.keys + ['system'] + @conf.keys end end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 449657f8cab..0cd7fc9f44a 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -27,7 +27,7 @@ %title= html_title = stylesheet_pack_tag 'common', media: 'all', crossorigin: 'anonymous' - = stylesheet_pack_tag current_theme, media: 'all', crossorigin: 'anonymous' + = theme_style_tags current_theme -# Needed for the wicg-inert polyfill. It needs to be on it's own