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.