PageSourceSearch

https://triunitystudios.com/web-projects/whatsapp-no-contact/recents.js

js triunitystudios.com collected 2026-10-02 01:47:14 UTC 2,986 bytes, 86 lines download raw bytes

1// The list of numbers this browser used before. Private to the device and
2// never shared, so it lives in localStorage per root ADR 0007. This module owns
3// the stored format and validates everything it reads back; `app.js` only calls
4// getItem and setItem.
5
6import { digitsOnly, normalizeNational, toE164Digits, validateNumber } from "./phone.js";
7
8export const RECENTS_KEY = "whatsapp-no-contact:recents:v1";
9export const MAX_RECENTS = 6;
10
11/**
12 * Accept a candidate only if it is a usable number, and return it in the one
13 * stored shape. Anything else becomes null, so old or corrupt stored data can
14 * never reach the page.
15 * @param {*} candidate
16 * @returns {{dial: string, national: string}|null}
17 */
18function toEntry(candidate) {
19  if (candidate === null || typeof candidate !== "object") return null;
20  const dial = digitsOnly(candidate.dial);
21  const state = { dial, national: candidate.national };
22  if (!validateNumber(state).valid) return null;
23  return { dial, national: normalizeNational(candidate.national, dial) };
24}
25
26/**
27 * Put a number at the front of the list. A number already in the list moves up
28 * instead of appearing twice, and the list never grows past the cap.
29 * @param {Array} list the current list
30 * @param {{dial: ?string, national: ?string}} entry the number just used
31 * @param {number} [max] how many numbers to keep
32 * @returns {Array<{dial: string, national: string}>} a new list
33 */
34export function addRecent(list, entry, max = MAX_RECENTS) {
35  const current = Array.isArray(list) ? list : [];
36  const normalized = toEntry(entry);
37  if (normalized === null) return current.slice();
38
39  const key = toE164Digits(normalized);
40  const kept = current.filter((item) => {
41    const existing = toEntry(item);
42    return existing !== null && toE164Digits(existing) !== key;
43  });
44  return [normalized, ...kept].slice(0, max);
45}
46
47/**
48 * @param {Array} list
49 * @returns {string} JSON holding only the dial code and the number of each
50 *   valid entry
51 */
52export function serializeRecents(list) {
53  const entries = (Array.isArray(list) ? list : []).map(toEntry).filter((entry) => entry !== null);
54  return JSON.stringify(entries);
55}
56
57/**
58 * Read the stored list defensively: bad JSON, a value that is not a list,
59 * unusable entries, duplicates and overflow are all dropped.
60 * @param {?string} raw what localStorage returned
61 * @returns {Array<{dial: string, national: string}>}
62 */
63export function deserializeRecents(raw) {
64  if (typeof raw !== "string" || raw.trim() === "") return [];
65
66  let parsed;
67  try {
68    parsed = JSON.parse(raw);
69  } catch {
70    return [];
71  }
72  if (!Array.isArray(parsed)) return [];
73
74  const entries = [];
75  const seen = new Set();
76  for (const candidate of parsed) {
77    const entry = toEntry(candidate);
78    if (entry === null) continue;
79    const key = toE164Digits(entry);
80    if (seen.has(key)) continue;
81    seen.add(key);
82    entries.push(entry);
83    if (entries.length === MAX_RECENTS) break;
84  }
85  return entries;
86}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.