feat(conversation-viewer): update message rendering with single emoji detection and dynamic font sizing for improved display

This commit is contained in:
Ivan
2026-04-13 22:58:04 -05:00
parent dc280c13bd
commit 290f7e09d4
3 changed files with 71 additions and 1 deletions

View File

@@ -818,10 +818,11 @@
'markdown-content--outbound-solid':
chatItem.is_outbound && !isThemeOutboundBubble(chatItem),
'markdown-content--inbound': !chatItem.is_outbound,
'markdown-content--single-emoji': messageMarkdownSingleEmoji(chatItem),
}"
:style="{
'font-family': 'inherit',
'font-size': (config?.message_font_size || 14) + 'px',
'font-size': messageMarkdownFontSizePx(chatItem) + 'px',
}"
@click="handleMessageClick"
v-html="renderMarkdown(chatItem.lxmf_message.content)"
@@ -2989,6 +2990,26 @@ export default {
renderMarkdown(text) {
return MarkdownRenderer.render(text);
},
messageMarkdownSingleEmoji(chatItem) {
const c = chatItem?.lxmf_message?.content;
if (!c) {
return false;
}
if (this.getParsedItems(chatItem)?.isOnlyPaperMessage) {
return false;
}
if (this.shouldHideAutoImageCaption(chatItem)) {
return false;
}
return MarkdownRenderer.isSingleEmojiMessage(c);
},
messageMarkdownFontSizePx(chatItem) {
const base = Number(this.config?.message_font_size) || 14;
if (this.messageMarkdownSingleEmoji(chatItem)) {
return Math.round(base * 2.75);
}
return base;
},
handleMessageClick(event) {
const nomadnetLink = event.target.closest(".nomadnet-link");
if (nomadnetLink) {
@@ -5724,6 +5745,15 @@ export default {
margin: 0.5rem 0;
}
.markdown-content--single-emoji {
line-height: 1;
}
.markdown-content--single-emoji :deep(p) {
margin: 0;
line-height: 1;
}
.markdown-content :deep(strong) {
font-weight: 700;
}

View File

@@ -80,6 +80,30 @@ export default class MarkdownRenderer {
return processed_parts.join("\n");
}
/**
* True when the body is only a single emoji (after markdown strip), for large bubble rendering.
*/
static isSingleEmojiMessage(raw) {
if (raw == null || typeof raw !== "string") {
return false;
}
let plain = MarkdownRenderer.strip(raw);
plain = plain.replace(/\s+/g, "");
if (!plain) {
return false;
}
if (typeof Intl === "undefined" || typeof Intl.Segmenter !== "function") {
return false;
}
const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" });
const clusters = [...seg.segment(plain)].map((s) => s.segment);
if (clusters.length !== 1) {
return false;
}
const g = clusters[0];
return /\p{Extended_Pictographic}/u.test(g) || /\p{Emoji}/u.test(g);
}
/**
* Strips markdown from text for previews.
*/

View File

@@ -226,6 +226,22 @@ describe("MarkdownRenderer.js", () => {
});
});
describe("isSingleEmojiMessage", () => {
it("is true for one emoji and false for text or multiple emojis", () => {
expect(MarkdownRenderer.isSingleEmojiMessage("\u{1F600}")).toBe(true);
expect(MarkdownRenderer.isSingleEmojiMessage(" \u{1F600} ")).toBe(true);
expect(MarkdownRenderer.isSingleEmojiMessage("**\u{1F600}**")).toBe(true);
expect(MarkdownRenderer.isSingleEmojiMessage("\u{1F600}\u{1F600}")).toBe(false);
expect(MarkdownRenderer.isSingleEmojiMessage("hi")).toBe(false);
expect(MarkdownRenderer.isSingleEmojiMessage("\u{1F600} a")).toBe(false);
});
it("treats ZWJ family and skin tone as a single emoji", () => {
expect(MarkdownRenderer.isSingleEmojiMessage("\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}")).toBe(true);
expect(MarkdownRenderer.isSingleEmojiMessage("\u{1F44D}\u{1F3FD}")).toBe(true);
});
});
describe("strip", () => {
it("strips markdown correctly", () => {
const md = "# Header\n**Bold** *Italic* `code` ```\nblock\n```";