@leg3ndy/otto-bridge 0.7.5 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2366,6 +2366,9 @@ function isVisible(element) {
2366
2366
 
2367
2367
  function focusAndReplaceContent(element, value) {
2368
2368
  element.focus();
2369
+ if (typeof element.click === "function") {
2370
+ element.click();
2371
+ }
2369
2372
  const range = document.createRange();
2370
2373
  range.selectNodeContents(element);
2371
2374
  const selection = window.getSelection();
@@ -2373,7 +2376,14 @@ function focusAndReplaceContent(element, value) {
2373
2376
  selection?.addRange(range);
2374
2377
  document.execCommand("selectAll", false);
2375
2378
  document.execCommand("delete", false);
2379
+ if ((element.innerText || element.textContent || "").trim().length > 0) {
2380
+ element.textContent = "";
2381
+ element.dispatchEvent(new InputEvent("input", { bubbles: true, data: "", inputType: "deleteContentBackward" }));
2382
+ }
2376
2383
  document.execCommand("insertText", false, value);
2384
+ if ((element.innerText || "").trim() !== value.trim()) {
2385
+ element.textContent = value;
2386
+ }
2377
2387
  element.dispatchEvent(new InputEvent("input", { bubbles: true, data: value, inputType: "insertText" }));
2378
2388
  }
2379
2389
 
@@ -2418,18 +2428,36 @@ function isVisible(element) {
2418
2428
  return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
2419
2429
  }
2420
2430
 
2431
+ function pickClickTarget(node) {
2432
+ const selectors = [
2433
+ '[role="gridcell"]',
2434
+ '[role="listitem"]',
2435
+ '[data-testid="cell-frame-container"]',
2436
+ 'div[tabindex]',
2437
+ ];
2438
+ for (const selector of selectors) {
2439
+ const candidate = node.closest(selector);
2440
+ if (candidate instanceof HTMLElement && isVisible(candidate)) {
2441
+ return candidate;
2442
+ }
2443
+ }
2444
+ return node instanceof HTMLElement && isVisible(node) ? node : null;
2445
+ }
2446
+
2421
2447
  const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
2422
2448
  .filter((node) => node instanceof HTMLElement)
2423
2449
  .filter((node) => isVisible(node))
2424
2450
  .map((node) => {
2425
2451
  const text = normalize(node.getAttribute("title") || node.textContent || "");
2452
+ const target = pickClickTarget(node);
2426
2453
  let score = 0;
2427
2454
  if (text === normalizedQuery) score += 160;
2428
2455
  if (text.includes(normalizedQuery)) score += 100;
2429
2456
  if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
2430
- const container = node.closest('[data-testid="cell-frame-container"], [role="listitem"], [role="gridcell"], div[tabindex]');
2431
- if (container instanceof HTMLElement && isVisible(container)) score += 20;
2432
- return { node, container, text, score };
2457
+ if (target instanceof HTMLElement && target !== node) score += 20;
2458
+ if (target instanceof HTMLElement && target.getAttribute("role") === "gridcell") score += 30;
2459
+ if (target instanceof HTMLElement && target.getAttribute("role") === "listitem") score += 20;
2460
+ return { node, target, text, score };
2433
2461
  })
2434
2462
  .filter((item) => item.score > 0)
2435
2463
  .sort((left, right) => right.score - left.score);
@@ -2439,12 +2467,10 @@ if (!titleNodes.length) {
2439
2467
  }
2440
2468
 
2441
2469
  const winner = titleNodes[0];
2442
- const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
2470
+ const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
2443
2471
  target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
2444
- if (typeof target.click === "function") {
2445
- target.click();
2446
- return { clicked: true };
2447
- }
2472
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
2473
+ target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
2448
2474
  target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
2449
2475
  return { clicked: true };
2450
2476
  `, { contact }, this.getWhatsAppWebScriptOptions(false));
@@ -275,35 +275,37 @@ export class MacOSWhatsAppHelperRuntime {
275
275
 
276
276
  const searchBox = candidates[0].node;
277
277
  searchBox.focus();
278
+ if (typeof searchBox.click === "function") {
279
+ searchBox.click();
280
+ }
278
281
 
279
282
  if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
280
283
  searchBox.value = "";
281
284
  searchBox.dispatchEvent(new Event("input", { bubbles: true }));
282
285
  searchBox.value = contact;
283
286
  searchBox.dispatchEvent(new Event("input", { bubbles: true }));
287
+ return { ok: true, nativeInputRequired: false };
284
288
  } else {
285
- const selection = window.getSelection();
286
- const range = document.createRange();
287
- range.selectNodeContents(searchBox);
288
- selection?.removeAllRanges();
289
- selection?.addRange(range);
290
- document.execCommand("selectAll", false);
291
- document.execCommand("delete", false);
292
- document.execCommand("insertText", false, contact);
293
- if ((searchBox.innerText || "").trim() !== contact.trim()) {
294
- searchBox.textContent = contact;
295
- }
289
+ searchBox.textContent = "";
296
290
  searchBox.dispatchEvent(new Event("input", { bubbles: true }));
291
+ return { ok: true, nativeInputRequired: true };
297
292
  }
298
-
299
- return { ok: true };
300
293
  })()
301
294
  `);
302
295
  if (!(prepared.ok === true)) {
303
296
  return false;
304
297
  }
298
+ if (prepared.nativeInputRequired === true) {
299
+ await this.nativeClearText().catch(() => false);
300
+ const inserted = await this.nativeInsertText(contact).catch(() => false);
301
+ if (!inserted) {
302
+ return false;
303
+ }
304
+ }
305
+ await delay(1000);
305
306
  let enterTriggered = false;
306
- let clickFallbackTriggered = false;
307
+ let clickAttempts = 0;
308
+ const searchStartedAt = Date.now();
307
309
  const deadline = Date.now() + 6_000;
308
310
  while (Date.now() < deadline) {
309
311
  await delay(500);
@@ -311,7 +313,8 @@ export class MacOSWhatsAppHelperRuntime {
311
313
  (() => {
312
314
  const query = ${JSON.stringify(contact)};
313
315
  const enterTriggered = ${JSON.stringify(enterTriggered)};
314
- const clickFallbackTriggered = ${JSON.stringify(clickFallbackTriggered)};
316
+ const clickAttempts = ${JSON.stringify(clickAttempts)};
317
+ const allowEnterFallback = ${JSON.stringify(Date.now() - searchStartedAt >= 2_500)};
315
318
  const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
316
319
  const normalizedQuery = normalize(query);
317
320
 
@@ -324,77 +327,107 @@ export class MacOSWhatsAppHelperRuntime {
324
327
  return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
325
328
  }
326
329
 
330
+ function pickClickTarget(node) {
331
+ const selectors = [
332
+ '[role="gridcell"]',
333
+ '[role="listitem"]',
334
+ '[data-testid="cell-frame-container"]',
335
+ 'div[tabindex]',
336
+ ];
337
+ for (const selector of selectors) {
338
+ const candidate = node.closest(selector);
339
+ if (candidate instanceof HTMLElement && isVisible(candidate)) {
340
+ return candidate;
341
+ }
342
+ }
343
+ return node instanceof HTMLElement && isVisible(node) ? node : null;
344
+ }
345
+
327
346
  function isConversationOpen() {
328
347
  const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
329
348
  .some((node) => isVisible(node));
330
349
  const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
331
350
  .some((node) => isVisible(node));
332
- const headerMatch = Array.from(document.querySelectorAll('header span[title], header div[title], main span[title], main div[title]'))
351
+ const headerMatch = Array.from(document.querySelectorAll('main header span[title], main header div[title], [data-testid="conversation-info-header-chat-title"], header span[title], header div[title], main span[title], main div[title]'))
333
352
  .filter((node) => node instanceof HTMLElement)
334
353
  .filter((node) => isVisible(node))
335
354
  .some((node) => {
336
355
  const text = normalize(node.getAttribute("title") || node.textContent || "");
337
- return text === normalizedQuery || text.includes(normalizedQuery);
356
+ return text === normalizedQuery
357
+ || text.includes(normalizedQuery)
358
+ || (normalizedQuery.includes(text) && text.length >= 3);
338
359
  });
339
- return footerVisible || composerVisible || headerMatch;
360
+ const composerTargetMatch = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"] [contenteditable="true"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
361
+ .filter((node) => node instanceof HTMLElement)
362
+ .filter((node) => isVisible(node))
363
+ .some((node) => {
364
+ const text = normalize(
365
+ node.getAttribute("aria-label")
366
+ || node.getAttribute("aria-placeholder")
367
+ || node.getAttribute("placeholder")
368
+ || ""
369
+ );
370
+ return text.includes(normalizedQuery);
371
+ });
372
+ return (headerMatch || composerTargetMatch) && (footerVisible || composerVisible);
340
373
  }
341
374
 
342
375
  if (isConversationOpen()) {
343
376
  return { clicked: true, activatedBy: "already-open" };
344
377
  }
345
378
 
346
- const searchCandidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], input[role="textbox"], textarea, div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
347
- .filter((node) => node instanceof HTMLElement)
348
- .filter((node) => isVisible(node));
349
- const searchBox = searchCandidates[0];
350
- if (!enterTriggered && searchBox instanceof HTMLElement) {
351
- searchBox.focus();
352
- searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
353
- searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
354
- searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
355
- return { clicked: false, enterTriggered: true };
356
- }
357
-
358
379
  const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
359
380
  .filter((node) => node instanceof HTMLElement)
360
381
  .filter((node) => isVisible(node))
361
382
  .map((node) => {
362
383
  const text = normalize(node.getAttribute("title") || node.textContent || "");
384
+ const target = pickClickTarget(node);
363
385
  let score = 0;
364
386
  if (text === normalizedQuery) score += 160;
365
387
  if (text.includes(normalizedQuery)) score += 100;
366
388
  if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
367
- const container = node.closest('[data-testid="cell-frame-container"], [role="listitem"], [role="gridcell"], div[tabindex]');
368
- if (container instanceof HTMLElement && isVisible(container)) score += 20;
369
- return { node, container, score };
389
+ if (target instanceof HTMLElement && target !== node) score += 20;
390
+ if (target instanceof HTMLElement && target.getAttribute("role") === "gridcell") score += 30;
391
+ if (target instanceof HTMLElement && target.getAttribute("role") === "listitem") score += 20;
392
+ return { node, target, score };
370
393
  })
371
394
  .filter((item) => item.score > 0)
372
395
  .sort((left, right) => right.score - left.score);
373
396
 
374
- if (!titleNodes.length) {
375
- return { clicked: false };
376
- }
397
+ if (titleNodes.length) {
398
+ if (clickAttempts >= 4) {
399
+ return { clicked: false };
400
+ }
377
401
 
378
- if (clickFallbackTriggered) {
379
- return { clicked: false };
402
+ const winner = titleNodes[0];
403
+ const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
404
+ target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
405
+ target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
406
+ target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
407
+ target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
408
+ return { clicked: false, clickAttempts: clickAttempts + 1 };
380
409
  }
381
410
 
382
- const winner = titleNodes[0];
383
- const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
384
- target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
385
- if (typeof target.click === "function") {
386
- target.click();
387
- return { clicked: false, clickFallbackTriggered: true };
411
+ const searchCandidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], input[role="textbox"], textarea, div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
412
+ .filter((node) => node instanceof HTMLElement)
413
+ .filter((node) => isVisible(node));
414
+ const searchBox = searchCandidates[0];
415
+ if (allowEnterFallback && !enterTriggered && searchBox instanceof HTMLElement) {
416
+ searchBox.focus();
417
+ searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
418
+ searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
419
+ searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
420
+ return { clicked: false, enterTriggered: true };
388
421
  }
389
- target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
390
- return { clicked: false, clickFallbackTriggered: true };
422
+
423
+ return { clicked: false };
391
424
  })()
392
425
  `);
393
426
  if (result.enterTriggered === true) {
394
427
  enterTriggered = true;
395
428
  }
396
- if (result.clickFallbackTriggered === true) {
397
- clickFallbackTriggered = true;
429
+ if (typeof result.clickAttempts === "number") {
430
+ clickAttempts = result.clickAttempts;
398
431
  }
399
432
  if (result.clicked === true) {
400
433
  return true;
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.7.5";
2
+ export const BRIDGE_VERSION = "0.7.6";
3
3
  export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
4
4
  export const DEFAULT_API_BASE_URL = "http://localhost:8000";
5
5
  export const DEFAULT_POLL_INTERVAL_MS = 3000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",