From 044cb495ce624fd53cf3fdc8de01e9f9428609c0 Mon Sep 17 00:00:00 2001 From: ohmwraith Date: Sun, 26 Apr 2026 02:05:57 +0300 Subject: [PATCH 1/3] Restore emoji textures in nicknames, mentions and roles --- src/webpage/markdown.ts | 10 ++++++++++ src/webpage/member.ts | 4 +++- src/webpage/role.ts | 3 ++- src/webpage/style.css | 3 +++ src/webpage/user.ts | 5 +++-- src/webpage/utils/utils.ts | 29 +++++++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts index dad46bc8..c09afe9f 100644 --- a/src/webpage/markdown.ts +++ b/src/webpage/markdown.ts @@ -5,6 +5,7 @@ import {Guild} from "./guild.js"; import {I18n} from "./i18n.js"; import {Dialog} from "./settings.js"; import {Contextmenu} from "./contextmenu.js"; +import {setTextWithWrappedEmoji} from "./utils/utils.js"; const linkMenu = new Contextmenu("copyLink", true); linkMenu.addButton( () => I18n.copyRegLink(), @@ -629,6 +630,7 @@ class MarkDown { mention.classList.add("mentionMD"); mention.contentEditable = "false"; mention.textContent = everyone ? "@everyone" : "@here"; + setTextWithWrappedEmoji(mention, mention.textContent || ""); appendcurrent(); span.appendChild(mention); mention.setAttribute("real", everyone ? `@everyone` : "@here"); @@ -670,17 +672,21 @@ class MarkDown { const role = this.channel.guild.roleids.get(id); if (role) { mention.textContent = `@${role.name}`; + setTextWithWrappedEmoji(mention, mention.textContent || ""); mention.style.color = `var(--role-${role.id})`; } else { mention.textContent = I18n.guild.unknownRole(); + setTextWithWrappedEmoji(mention, mention.textContent || ""); } } } else { (async () => { mention.textContent = I18n.userping.resolving(); + setTextWithWrappedEmoji(mention, mention.textContent || ""); const user = await this.localuser?.getUser(id); if (user) { mention.textContent = `@${user.name}`; + setTextWithWrappedEmoji(mention, mention.textContent || ""); let guild: null | Guild = null; if (this.channel) { guild = this.channel.guild; @@ -692,11 +698,13 @@ class MarkDown { guild.resolveMember(user).then((member) => { if (member) { mention.textContent = `@${member.name}`; + setTextWithWrappedEmoji(mention, mention.textContent || ""); } }); } } else { mention.textContent = I18n.userping.unknown(); + setTextWithWrappedEmoji(mention, mention.textContent || ""); } })(); } @@ -705,6 +713,7 @@ class MarkDown { const channel = this.localuser.channelids.get(id); if (channel) { mention.textContent = `#${channel.name}`; + setTextWithWrappedEmoji(mention, mention.textContent || ""); if (!keep && !stdsize) { mention.onclick = (_) => { if (!this.localuser) return; @@ -713,6 +722,7 @@ class MarkDown { } } else { mention.textContent = "#unknown"; + setTextWithWrappedEmoji(mention, mention.textContent || ""); } break; } diff --git a/src/webpage/member.ts b/src/webpage/member.ts index 0277bc06..b6d7fc5c 100644 --- a/src/webpage/member.ts +++ b/src/webpage/member.ts @@ -6,6 +6,7 @@ import {highMemberJSON, memberjson, presencejson} from "./jsontypes.js"; import {I18n} from "./i18n.js"; import {Dialog, Options, Settings} from "./settings.js"; import {CDNParams} from "./utils/cdnParams.js"; +import {setTextWithWrappedEmoji} from "./utils/utils.js"; class Member extends SnowFlake { static already = {}; @@ -34,6 +35,7 @@ class Member extends SnowFlake { elms = new Set>(); subName(elm: HTMLElement) { this.elms.add(new WeakRef(elm)); + setTextWithWrappedEmoji(elm, this.name); } nameChange() { for (const ref of this.elms) { @@ -42,7 +44,7 @@ class Member extends SnowFlake { this.elms.delete(ref); continue; } - elm.textContent = this.name; + setTextWithWrappedEmoji(elm, this.name); } } commuicationDisabledLeft() { diff --git a/src/webpage/role.ts b/src/webpage/role.ts index f9f65f45..5a8dd697 100644 --- a/src/webpage/role.ts +++ b/src/webpage/role.ts @@ -8,6 +8,7 @@ import {OptionsElement, Buttons, Dialog, ColorInput} from "./settings.js"; import {Contextmenu} from "./contextmenu.js"; import {Channel} from "./channel.js"; import {I18n} from "./i18n.js"; +import {setTextWithWrappedEmoji} from "./utils/utils.js"; class Role extends SnowFlake { permissions: Permissions; @@ -647,7 +648,7 @@ class RoleList extends Buttons { this.buttonMap.set(thing[0], button); button.classList.add("SettingsButton"); const span = document.createElement("span"); - span.textContent = thing[0]; + setTextWithWrappedEmoji(span, thing[0]); button.append(span); span.classList.add("roleButtonStyle"); const role = this.guild.roleids.get(thing[1]) || this.guild.localuser.userMap.get(thing[1]); diff --git a/src/webpage/style.css b/src/webpage/style.css index 07fc8e18..548201d6 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -2646,6 +2646,9 @@ span.instanceStatus { object-fit: contain; align-self: center; } +span.emoji { + color: initial; +} .userwrap { display: flex; align-items: baseline; diff --git a/src/webpage/user.ts b/src/webpage/user.ts index ca074874..ca5d20f6 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -17,7 +17,7 @@ import {Search} from "./search.js"; import {I18n} from "./i18n.js"; import {Hover} from "./hover.js"; import {Dialog, Float, Options} from "./settings.js"; -import {createImg, removeAni, safeImg} from "./utils/utils.js"; +import {createImg, removeAni, safeImg, setTextWithWrappedEmoji} from "./utils/utils.js"; import {Direct} from "./direct.js"; import {Permissions} from "./permissions.js"; import {Channel} from "./channel.js"; @@ -682,6 +682,7 @@ class User extends SnowFlake { elms = new Set>(); subName(elm: HTMLElement) { this.elms.add(new WeakRef(elm)); + setTextWithWrappedEmoji(elm, this.name); } nameChange() { this.getMembersSync().forEach((memb) => { @@ -694,7 +695,7 @@ class User extends SnowFlake { this.elms.delete(ref); continue; } - elm.textContent = this.name; + setTextWithWrappedEmoji(elm, this.name); } } diff --git a/src/webpage/utils/utils.ts b/src/webpage/utils/utils.ts index ffb35ca5..15ed9291 100644 --- a/src/webpage/utils/utils.ts +++ b/src/webpage/utils/utils.ts @@ -31,6 +31,35 @@ let instances: }[] | null = null; await setTheme(); +const emojiGraphemeRegex = /^(?:[0-9#*]\uFE0F?\u20E3|\p{Regional_Indicator}{2}|\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?[\u{1F3FB}-\u{1F3FF}]?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?[\u{1F3FB}-\u{1F3FF}]?)*?)$/u; +export function setTextWithWrappedEmoji(target: HTMLElement, name: string) { + target.textContent = ""; + + if (!("Segmenter" in Intl)) { + target.textContent = name; + return; + } + + const segmenter = new Intl.Segmenter("und", {granularity: "grapheme"}); + let textBuffer = ""; + const flushTextBuffer = () => { + if (!textBuffer) return; + target.append(textBuffer); + textBuffer = ""; + }; + for (const {segment} of segmenter.segment(name)) { + if (emojiGraphemeRegex.test(segment)) { + flushTextBuffer(); + const emoji = document.createElement("span"); + emoji.classList.add("emoji"); + emoji.textContent = segment; + target.append(emoji); + } else { + textBuffer += segment; + } + } + flushTextBuffer(); +} export async function setTheme(theme?: string) { const prefs = await getPreferences(); document.body.className = (theme || prefs.theme) + "-theme"; From b6eca7e471248fcd74e5b530abad617337910dc4 Mon Sep 17 00:00:00 2001 From: ohmwraith Date: Sun, 26 Apr 2026 03:15:15 +0300 Subject: [PATCH 2/3] Fix role mention custom color --- src/webpage/markdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts index c09afe9f..fa516924 100644 --- a/src/webpage/markdown.ts +++ b/src/webpage/markdown.ts @@ -673,7 +673,7 @@ class MarkDown { if (role) { mention.textContent = `@${role.name}`; setTextWithWrappedEmoji(mention, mention.textContent || ""); - mention.style.color = `var(--role-${role.id})`; + mention.style.setProperty("--userbg", `var(--role-${role.id})`); } else { mention.textContent = I18n.guild.unknownRole(); setTextWithWrappedEmoji(mention, mention.textContent || ""); From c2cf614142580511dfb8cc18882a8e1e04cd16c8 Mon Sep 17 00:00:00 2001 From: ohmwraith Date: Sun, 26 Apr 2026 19:44:06 +0300 Subject: [PATCH 3/3] Add comments for emoji wrapping --- src/webpage/markdown.ts | 2 +- src/webpage/utils/utils.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts index fa516924..3972c0d3 100644 --- a/src/webpage/markdown.ts +++ b/src/webpage/markdown.ts @@ -630,7 +630,7 @@ class MarkDown { mention.classList.add("mentionMD"); mention.contentEditable = "false"; mention.textContent = everyone ? "@everyone" : "@here"; - setTextWithWrappedEmoji(mention, mention.textContent || ""); + appendcurrent(); span.appendChild(mention); mention.setAttribute("real", everyone ? `@everyone` : "@here"); diff --git a/src/webpage/utils/utils.ts b/src/webpage/utils/utils.ts index 15ed9291..4f10a0a6 100644 --- a/src/webpage/utils/utils.ts +++ b/src/webpage/utils/utils.ts @@ -31,16 +31,17 @@ let instances: }[] | null = null; await setTheme(); +// Matches keycaps, flags, standard, skin-toned, and ZWJ emoji sequences. const emojiGraphemeRegex = /^(?:[0-9#*]\uFE0F?\u20E3|\p{Regional_Indicator}{2}|\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?[\u{1F3FB}-\u{1F3FF}]?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?[\u{1F3FB}-\u{1F3FF}]?)*?)$/u; export function setTextWithWrappedEmoji(target: HTMLElement, name: string) { target.textContent = ""; - + // Fallback for browsers without grapheme segmentation support. if (!("Segmenter" in Intl)) { target.textContent = name; return; } - const segmenter = new Intl.Segmenter("und", {granularity: "grapheme"}); + // Buffer plain text between emoji to keep fewer DOM nodes. let textBuffer = ""; const flushTextBuffer = () => { if (!textBuffer) return;