Skip to content

Commit c732c9c

Browse files
committed
fix(flatpak): ensure emojis are rendered correctly on every launch
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
1 parent d7439e7 commit c732c9c

File tree

5 files changed

+115
-1
lines changed

5 files changed

+115
-1
lines changed

src/app/system.utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export async function clearFlatpakFontConfigCache() {
146146
}
147147

148148
try {
149+
console.debug('Clearing Flatpak font config cache...')
149150
// Note: clearing with "fc-cache" command did not help with the issue (was tested with many users and colleagues)
150151
await rm(path.join(process.env.XDG_CACHE_HOME, 'fontconfig'), { recursive: true, force: true })
151152
} catch (error) {

src/main.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const { triggerDownloadUrl } = require('./app/downloads.ts')
1818
const { setupReleaseNotificationScheduler, checkForUpdate } = require('./app/githubRelease.service.ts')
1919
const { initLaunchAtStartupListener } = require('./app/launchAtStartup.config.ts')
2020
const { runMigrations } = require('./app/migration.service.ts')
21-
const { systemInfo, isLinux, isMac, isWindows, isSameExecution, isSquirrel, relaunchApp } = require('./app/system.utils.ts')
21+
const { systemInfo, isLinux, isMac, isWindows, isSameExecution, isSquirrel, relaunchApp, clearFlatpakFontConfigCache } = require('./app/system.utils.ts')
2222
const { applyTheme } = require('./app/theme.config.ts')
2323
const { buildTitle } = require('./app/utils.ts')
2424
const { enableWebRequestInterceptor, disableWebRequestInterceptor } = require('./app/webRequestInterceptor.js')
@@ -92,6 +92,7 @@ ipcMain.on('app:grantUserGesturedPermission', (event, id) => {
9292
return event.sender.executeJavaScript(`document.getElementById('${id}')?.click()`, true)
9393
})
9494
ipcMain.on('app:toggleDevTools', (event) => event.sender.toggleDevTools())
95+
ipcMain.on('app:clearFlatpakFontConfigCache', async () => clearFlatpakFontConfigCache())
9596
ipcMain.handle('app:anything', () => { /* Put any code here to run it from UI */ })
9697
ipcMain.on('app:openChromeWebRtcInternals', () => openChromeWebRtcInternals())
9798
ipcMain.handle('app:update:check', async () => await checkForUpdate({ forceRequest: true }))

src/preload.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ const TALK_DESKTOP = {
125125
* Open developer tools
126126
*/
127127
toggleDevTools: () => ipcRenderer.send('app:toggleDevTools'),
128+
/**
129+
* Clear Flatpak fontconfig cache
130+
*/
131+
clearFlatpakFontConfigCache: () => ipcRenderer.send('app:clearFlatpakFontConfigCache'),
128132
/**
129133
* Invoke app:anything
130134
*
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { getBuilder } from '@nextcloud/browser-storage'
7+
8+
/**
9+
* Cool down time after the last attempt to clear the Flatpak font config cache preventing infinite loop in case of failed fix
10+
*/
11+
const FLATPAK_FONT_CONFIG_CACHE_CLEAR_COOL_DOWN = 24 * 60 * 60 * 1000 // 24h
12+
13+
const LAST_FLATPAK_FONT_CONFIG_CACHE_CLEAR_KEY = 'lastFlatpakFontConfigCacheClear'
14+
15+
/**
16+
* Ensure there is no emoji rendering issue in Flatpak installation resulting in emojis being rendered as text instead of colored.
17+
* If there is, clear the Flatpak font config cache and relaunch the app.
18+
*/
19+
export async function ensureFlatpakEmojiFontRendering() {
20+
// Flatpak specific issue only
21+
if (!window.systemInfo.isFlatpak) {
22+
return
23+
}
24+
25+
// No issues - nothing to fix
26+
if (!hasEmojiRenderingIssue()) {
27+
return
28+
}
29+
30+
// Prevent the relaunch loop when there is an issue but it is not solved by clearing the cache and relaunch
31+
const browserStorage = getBuilder('talk-desktop').persist().build()
32+
const lastFontconfigCacheClear = browserStorage.getItem(LAST_FLATPAK_FONT_CONFIG_CACHE_CLEAR_KEY)
33+
34+
if (lastFontconfigCacheClear && (Date.now() - parseInt(lastFontconfigCacheClear)) < FLATPAK_FONT_CONFIG_CACHE_CLEAR_COOL_DOWN) {
35+
console.warn('Emoji rendering issue detected, but font config cache was cleared recently. Probably the issue is not solvable by clearing the cache...')
36+
return
37+
}
38+
39+
browserStorage.setItem(LAST_FLATPAK_FONT_CONFIG_CACHE_CLEAR_KEY, Date.now().toString())
40+
41+
await window.TALK_DESKTOP.clearFlatpakFontConfigCache()
42+
await window.TALK_DESKTOP.relaunch()
43+
}
44+
45+
/**
46+
* Check whether there is an emoji rendering issue resulting in emojis being rendered as text instead of colored images
47+
* by rendering an emoji on a canvas and checking how colorful it is.
48+
* The check cost is around 40ms.
49+
*/
50+
export function hasEmojiRenderingIssue(): boolean {
51+
const EMOJI = '😅'
52+
// Uncomment for testing of forced text emoji rendering
53+
// const EMOJI = '😅\uFE0E'
54+
55+
// Same as in EmojiPicker
56+
const FONT_FAMILY = '"Segoe UI Emoji","Segoe UI Symbol","Segoe UI","Apple Color Emoji","Twemoji Mozilla","Noto Color Emoji","EmojiOne Color","Android Emoji"'
57+
const FONT_SIZE = 15
58+
const WIDTH = 20
59+
const HEIGHT = 20
60+
61+
/**
62+
* How much colored an emoji must be to consider it successfully colored.
63+
* On testing, monochrome text emoji is always 0.0 and colored emoji is usually 0.4..0.6 with bright yellow Noto Color Emojis
64+
* Only very gray emojis like 😶‍🌫️ has low chroma and it is still >0.11
65+
*/
66+
const CHROMA_THRESHOLD = 0.1
67+
68+
const canvas = document.createElement('canvas')
69+
canvas.width = WIDTH
70+
canvas.height = HEIGHT
71+
// Uncomment for debugging
72+
// document.body.appendChild(canvas)
73+
74+
const ctx = canvas.getContext('2d')!
75+
ctx.fillStyle = '#000000'
76+
ctx.font = `${FONT_SIZE}px ${FONT_FAMILY}`
77+
ctx.textAlign = 'center'
78+
ctx.fillText(EMOJI, WIDTH / 2, FONT_SIZE, WIDTH)
79+
80+
const { data } = ctx.getImageData(0, 0, WIDTH, HEIGHT)
81+
82+
const chroma = imageChroma(data)
83+
console.debug('Flatpak emoji rendering test chroma:', chroma)
84+
85+
return chroma < CHROMA_THRESHOLD
86+
}
87+
88+
/**
89+
* Calculates the average chroma of the given pixel data, ignoring transparent parts
90+
*
91+
* @param pixels - RGBA pixel image data
92+
*/
93+
function imageChroma(pixels: Uint8ClampedArray): number {
94+
let totalChroma = 0
95+
let nonTransparentPixels = 0
96+
for (let i = 0; i < pixels.length; i += 4) {
97+
const [r, g, b, a] = [pixels[i]!, pixels[i + 1]!, pixels[i + 2]!, pixels[i + 3]!]
98+
if (a === 0) {
99+
continue
100+
}
101+
nonTransparentPixels += 1
102+
totalChroma += Math.max(r, g, b) - Math.min(r, g, b)
103+
}
104+
return totalChroma / nonTransparentPixels / 255
105+
}

src/welcome/welcome.main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { refetchAppDataWithRetry } from '../app/appData.service.js'
88
import { getAppConfigValue, initAppConfig, setAppConfigValue } from '../shared/appConfig.service.ts'
99
import { initGlobals } from '../shared/globals/globals.js'
1010
import { applyAxiosInterceptors } from '../shared/setupWebPage.js'
11+
import { ensureFlatpakEmojiFontRendering } from './ensureFlatpakEmojiFontRendering.ts'
1112

1213
import '@global-styles/dist/icons.css'
1314

@@ -30,6 +31,8 @@ appData.restore()
3031
initGlobals()
3132
applyAxiosInterceptors()
3233

34+
await ensureFlatpakEmojiFontRendering()
35+
3336
if (appData.credentials) {
3437
await window.TALK_DESKTOP.enableWebRequestInterceptor(appData.serverUrl, { credentials: appData.credentials })
3538
await refetchAppDataWithRetry(appData)

0 commit comments

Comments
 (0)