Handle rich emoji composer content

This commit is contained in:
sevenhill
2026-07-04 23:06:42 +03:00
parent 0468d02bf3
commit 05f971c5c7
+95 -24
View File
@@ -2985,9 +2985,9 @@ async function sendTextAndConfirm(p, externalChatId, text) {
return null;
}
const composerText = await readComposerText(p);
if (composerText !== null && !messageTextMatches(composerText, wanted)) {
console.warn(`[send/text] composer mismatch after fill; attempt=${attempt.name} composerLength=${composerText.length} wantedLength=${wanted.length}`);
const composerState = await readComposerState(p);
if (!composerStateMatchesText(composerState, text)) {
console.warn(`[send/text] composer mismatch after fill; attempt=${attempt.name} composerLength=${composerState?.text?.length ?? -1} richCount=${composerState?.richContentCount ?? -1} wantedLength=${wanted.length}`);
continue;
}
@@ -3010,8 +3010,8 @@ async function sendTextAndConfirm(p, externalChatId, text) {
return externalId;
}
const currentComposerText = await readComposerText(p);
if (currentComposerText !== null && currentComposerText.length === 0) {
const currentComposerState = await readComposerState(p);
if (composerStateIsEmpty(currentComposerState)) {
console.warn(`[send/text] MAX cleared composer but no outgoing message appeared; attempt=${attempt.name}`);
return null;
}
@@ -3040,7 +3040,10 @@ async function fillComposerText(p, text, clearFirst) {
await clearComposerInput(p);
}
if (await insertComposerTextViaDom(p, text, false)) {
return true;
await p.waitForTimeout(300);
if (composerStateMatchesText(await readComposerState(p), text)) {
return true;
}
}
if (clearFirst && await fillComposerViaLocator(p, text)) {
@@ -3059,8 +3062,8 @@ async function fillComposerText(p, text, clearFirst) {
await p.keyboard.insertText(text);
await p.waitForTimeout(300);
const currentText = await readComposerText(p);
if (currentText !== null && messageTextMatches(currentText, text)) {
const state = await readComposerState(p);
if (composerStateMatchesText(state, text)) {
return true;
}
}
@@ -3087,8 +3090,8 @@ async function fillComposerText(p, text, clearFirst) {
}
await p.keyboard.insertText(text);
await p.waitForTimeout(250);
const currentText = await readComposerText(p);
if (currentText !== null && messageTextMatches(currentText, text)) {
const state = await readComposerState(p);
if (composerStateMatchesText(state, text)) {
return true;
}
} catch {
@@ -3108,16 +3111,14 @@ async function clearComposerInput(p) {
await p.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A");
await p.keyboard.press("Backspace");
await p.waitForTimeout(120);
const currentText = await readComposerText(p);
if (currentText !== null && currentText.length === 0) {
if (composerStateIsEmpty(await readComposerState(p))) {
return true;
}
}
await insertComposerTextViaDom(p, "", true);
await p.waitForTimeout(150);
const currentText = await readComposerText(p);
return currentText !== null && currentText.length === 0;
return composerStateIsEmpty(await readComposerState(p));
}
async function fillComposerViaClipboard(p, text, clearFirst) {
@@ -3142,8 +3143,7 @@ async function fillComposerViaClipboard(p, text, clearFirst) {
if (wroteClipboard) {
await p.keyboard.press(process.platform === "darwin" ? "Meta+V" : "Control+V");
await p.waitForTimeout(300);
const pastedText = await readComposerText(p);
if (pastedText !== null && messageTextMatches(pastedText, text)) {
if (composerStateMatchesText(await readComposerState(p), text)) {
return true;
}
}
@@ -3164,8 +3164,7 @@ async function fillComposerViaKeyboard(p, text, clearFirst) {
await p.keyboard.insertText(text);
await p.waitForTimeout(300);
const currentText = await readComposerText(p);
return currentText !== null && messageTextMatches(currentText, text);
return composerStateMatchesText(await readComposerState(p), text);
}
async function insertComposerTextViaDom(p, text, clearFirst = false) {
@@ -3236,11 +3235,16 @@ async function insertComposerTextViaDom(p, text, clearFirst = false) {
if ("value" in composer) {
composer.value = "";
} else {
composer.textContent = "";
composer.replaceChildren();
}
composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null }));
}
if (!value) {
composer.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
const beforeInput = new InputEvent("beforeinput", {
bubbles: true,
cancelable: true,
@@ -3281,8 +3285,7 @@ async function fillComposerViaLocator(p, text) {
await composer.fill("", { timeout: 3000 });
await composer.fill(text, { timeout: 3000 });
await p.waitForTimeout(200);
const currentText = await readComposerText(p);
if (currentText !== null && messageTextMatches(currentText, text)) {
if (composerStateMatchesText(await readComposerState(p), text)) {
return true;
}
} catch {
@@ -3385,8 +3388,7 @@ async function focusComposer(p) {
async function waitForComposerToClear(p, timeoutMs = 4000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const text = await readComposerText(p);
if (text !== null && text.length === 0) {
if (composerStateIsEmpty(await readComposerState(p))) {
return true;
}
@@ -3397,6 +3399,11 @@ async function waitForComposerToClear(p, timeoutMs = 4000) {
}
async function readComposerText(p) {
const state = await readComposerState(p);
return state?.text ?? null;
}
async function readComposerState(p) {
return await p.evaluate(() => {
const cleanText = (value) => String(value || "")
.replace(/\u00a0/g, " ")
@@ -3404,6 +3411,7 @@ async function readComposerText(p) {
.replace(/\s+/g, " ")
.trim();
const isVisible = (element) => {
if (!(element instanceof Element)) return false;
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.visibility !== "hidden" &&
@@ -3458,10 +3466,73 @@ async function readComposerText(p) {
}
const value = "value" in composer ? composer.value : "";
return cleanText(value || composer.innerText || composer.textContent);
const text = cleanText(value || composer.innerText || composer.textContent);
const richContentCount = Array.from(composer.querySelectorAll([
"img",
"canvas",
"svg",
"[class*='emoji' i]",
"[class*='sticker' i]",
"[class*='lottie' i]",
"[class*='emoticon' i]",
"[data-testid*='emoji' i]",
"[data-testid*='sticker' i]",
"[data-testid*='lottie' i]"
].join(",")))
.filter((element) => isVisible(element))
.filter((element) => !/placeholder/i.test(String(element.className || "")))
.length;
return { text, richContentCount };
}).catch(() => null);
}
function composerStateIsEmpty(state) {
return Boolean(state && !state.text && (state.richContentCount || 0) === 0);
}
function composerStateMatchesText(state, wantedText) {
if (!state) {
return false;
}
if (messageTextMatches(state.text, wantedText)) {
return true;
}
if ((state.richContentCount || 0) === 0 || !containsEmojiSyntax(wantedText)) {
return false;
}
if (isEmojiOnlyText(wantedText)) {
return true;
}
const composerText = stripEmojiForComparison(state.text);
const wanted = stripEmojiForComparison(wantedText);
return Boolean(composerText && wanted && (
composerText === wanted ||
composerText.endsWith(wanted) ||
wanted.endsWith(composerText)
));
}
function containsEmojiSyntax(value) {
return /[\p{Extended_Pictographic}\u200d\ufe0f\ufe0e]/u.test(String(value || ""));
}
function isEmojiOnlyText(value) {
const text = cleanComparableText(value, 1000);
return Boolean(text && containsEmojiSyntax(text) && !stripEmojiForComparison(text));
}
function stripEmojiForComparison(value) {
return cleanComparableText(value, 1000)
.replace(/[\p{Extended_Pictographic}\u200d\ufe0f\ufe0e]/gu, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function messageTextMatches(value, wantedText) {
const text = cleanComparableText(value, 1000).toLowerCase();
const wanted = cleanComparableText(wantedText, 1000).toLowerCase();