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; return null;
} }
const composerText = await readComposerText(p); const composerState = await readComposerState(p);
if (composerText !== null && !messageTextMatches(composerText, wanted)) { if (!composerStateMatchesText(composerState, text)) {
console.warn(`[send/text] composer mismatch after fill; attempt=${attempt.name} composerLength=${composerText.length} wantedLength=${wanted.length}`); console.warn(`[send/text] composer mismatch after fill; attempt=${attempt.name} composerLength=${composerState?.text?.length ?? -1} richCount=${composerState?.richContentCount ?? -1} wantedLength=${wanted.length}`);
continue; continue;
} }
@@ -3010,8 +3010,8 @@ async function sendTextAndConfirm(p, externalChatId, text) {
return externalId; return externalId;
} }
const currentComposerText = await readComposerText(p); const currentComposerState = await readComposerState(p);
if (currentComposerText !== null && currentComposerText.length === 0) { if (composerStateIsEmpty(currentComposerState)) {
console.warn(`[send/text] MAX cleared composer but no outgoing message appeared; attempt=${attempt.name}`); console.warn(`[send/text] MAX cleared composer but no outgoing message appeared; attempt=${attempt.name}`);
return null; return null;
} }
@@ -3040,7 +3040,10 @@ async function fillComposerText(p, text, clearFirst) {
await clearComposerInput(p); await clearComposerInput(p);
} }
if (await insertComposerTextViaDom(p, text, false)) { 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)) { if (clearFirst && await fillComposerViaLocator(p, text)) {
@@ -3059,8 +3062,8 @@ async function fillComposerText(p, text, clearFirst) {
await p.keyboard.insertText(text); await p.keyboard.insertText(text);
await p.waitForTimeout(300); await p.waitForTimeout(300);
const currentText = await readComposerText(p); const state = await readComposerState(p);
if (currentText !== null && messageTextMatches(currentText, text)) { if (composerStateMatchesText(state, text)) {
return true; return true;
} }
} }
@@ -3087,8 +3090,8 @@ async function fillComposerText(p, text, clearFirst) {
} }
await p.keyboard.insertText(text); await p.keyboard.insertText(text);
await p.waitForTimeout(250); await p.waitForTimeout(250);
const currentText = await readComposerText(p); const state = await readComposerState(p);
if (currentText !== null && messageTextMatches(currentText, text)) { if (composerStateMatchesText(state, text)) {
return true; return true;
} }
} catch { } catch {
@@ -3108,16 +3111,14 @@ async function clearComposerInput(p) {
await p.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A"); await p.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A");
await p.keyboard.press("Backspace"); await p.keyboard.press("Backspace");
await p.waitForTimeout(120); await p.waitForTimeout(120);
const currentText = await readComposerText(p); if (composerStateIsEmpty(await readComposerState(p))) {
if (currentText !== null && currentText.length === 0) {
return true; return true;
} }
} }
await insertComposerTextViaDom(p, "", true); await insertComposerTextViaDom(p, "", true);
await p.waitForTimeout(150); await p.waitForTimeout(150);
const currentText = await readComposerText(p); return composerStateIsEmpty(await readComposerState(p));
return currentText !== null && currentText.length === 0;
} }
async function fillComposerViaClipboard(p, text, clearFirst) { async function fillComposerViaClipboard(p, text, clearFirst) {
@@ -3142,8 +3143,7 @@ async function fillComposerViaClipboard(p, text, clearFirst) {
if (wroteClipboard) { if (wroteClipboard) {
await p.keyboard.press(process.platform === "darwin" ? "Meta+V" : "Control+V"); await p.keyboard.press(process.platform === "darwin" ? "Meta+V" : "Control+V");
await p.waitForTimeout(300); await p.waitForTimeout(300);
const pastedText = await readComposerText(p); if (composerStateMatchesText(await readComposerState(p), text)) {
if (pastedText !== null && messageTextMatches(pastedText, text)) {
return true; return true;
} }
} }
@@ -3164,8 +3164,7 @@ async function fillComposerViaKeyboard(p, text, clearFirst) {
await p.keyboard.insertText(text); await p.keyboard.insertText(text);
await p.waitForTimeout(300); await p.waitForTimeout(300);
const currentText = await readComposerText(p); return composerStateMatchesText(await readComposerState(p), text);
return currentText !== null && messageTextMatches(currentText, text);
} }
async function insertComposerTextViaDom(p, text, clearFirst = false) { async function insertComposerTextViaDom(p, text, clearFirst = false) {
@@ -3236,11 +3235,16 @@ async function insertComposerTextViaDom(p, text, clearFirst = false) {
if ("value" in composer) { if ("value" in composer) {
composer.value = ""; composer.value = "";
} else { } else {
composer.textContent = ""; composer.replaceChildren();
} }
composer.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward", data: null })); 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", { const beforeInput = new InputEvent("beforeinput", {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
@@ -3281,8 +3285,7 @@ async function fillComposerViaLocator(p, text) {
await composer.fill("", { timeout: 3000 }); await composer.fill("", { timeout: 3000 });
await composer.fill(text, { timeout: 3000 }); await composer.fill(text, { timeout: 3000 });
await p.waitForTimeout(200); await p.waitForTimeout(200);
const currentText = await readComposerText(p); if (composerStateMatchesText(await readComposerState(p), text)) {
if (currentText !== null && messageTextMatches(currentText, text)) {
return true; return true;
} }
} catch { } catch {
@@ -3385,8 +3388,7 @@ async function focusComposer(p) {
async function waitForComposerToClear(p, timeoutMs = 4000) { async function waitForComposerToClear(p, timeoutMs = 4000) {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) { while (Date.now() < deadline) {
const text = await readComposerText(p); if (composerStateIsEmpty(await readComposerState(p))) {
if (text !== null && text.length === 0) {
return true; return true;
} }
@@ -3397,6 +3399,11 @@ async function waitForComposerToClear(p, timeoutMs = 4000) {
} }
async function readComposerText(p) { async function readComposerText(p) {
const state = await readComposerState(p);
return state?.text ?? null;
}
async function readComposerState(p) {
return await p.evaluate(() => { return await p.evaluate(() => {
const cleanText = (value) => String(value || "") const cleanText = (value) => String(value || "")
.replace(/\u00a0/g, " ") .replace(/\u00a0/g, " ")
@@ -3404,6 +3411,7 @@ async function readComposerText(p) {
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .trim();
const isVisible = (element) => { const isVisible = (element) => {
if (!(element instanceof Element)) return false;
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
return style.visibility !== "hidden" && return style.visibility !== "hidden" &&
@@ -3458,10 +3466,73 @@ async function readComposerText(p) {
} }
const value = "value" in composer ? composer.value : ""; 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); }).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) { function messageTextMatches(value, wantedText) {
const text = cleanComparableText(value, 1000).toLowerCase(); const text = cleanComparableText(value, 1000).toLowerCase();
const wanted = cleanComparableText(wantedText, 1000).toLowerCase(); const wanted = cleanComparableText(wantedText, 1000).toLowerCase();