diff --git a/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html b/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html index 6d7ec2805..89e8174a8 100644 --- a/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html +++ b/Sources/ContentScopeScripts/dist/pages/duckplayer/index.html @@ -72,7 +72,7 @@ padding: 16px; } -/* pages/duckplayer/app/components/Fallback.module.css */ +/* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; width: 100%; @@ -2119,7 +2119,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ @@ -3242,12 +3242,12 @@ return q2(UserValuesContext).setEnabled; } - // pages/duckplayer/app/components/Fallback.module.css + // shared/components/Fallback/Fallback.module.css var Fallback_default = { fallback: "Fallback_fallback" }; - // pages/duckplayer/app/components/Fallback.jsx + // shared/components/Fallback/Fallback.jsx function Fallback({ showDetails }) { return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); } diff --git a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css index de2906b8f..f0299a0ba 100644 --- a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css +++ b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.css @@ -48,7 +48,7 @@ body[data-display=app] { padding: 16px; } -/* pages/duckplayer/app/components/Fallback.module.css */ +/* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; width: 100%; diff --git a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js index 9fc0f424b..7a37e4e73 100644 --- a/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js +++ b/Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js @@ -1135,7 +1135,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ @@ -2258,12 +2258,12 @@ return q2(UserValuesContext).setEnabled; } - // pages/duckplayer/app/components/Fallback.module.css + // shared/components/Fallback/Fallback.module.css var Fallback_default = { fallback: "Fallback_fallback" }; - // pages/duckplayer/app/components/Fallback.jsx + // shared/components/Fallback/Fallback.jsx function Fallback({ showDetails }) { return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); } diff --git a/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js b/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js index d77743314..998401264 100644 --- a/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js +++ b/Sources/ContentScopeScripts/dist/pages/onboarding/js/index.js @@ -8440,7 +8440,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js b/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js index 29a845dc1..51ee5b01b 100644 --- a/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js +++ b/Sources/ContentScopeScripts/dist/pages/release-notes/js/index.js @@ -2051,7 +2051,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/Sources/ContentScopeScripts/dist/pages/special-error/index.html b/Sources/ContentScopeScripts/dist/pages/special-error/index.html index 19d2017fa..3777132f9 100644 --- a/Sources/ContentScopeScripts/dist/pages/special-error/index.html +++ b/Sources/ContentScopeScripts/dist/pages/special-error/index.html @@ -1917,7 +1917,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js b/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js index cb6005c4b..76e9c9512 100644 --- a/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js +++ b/Sources/ContentScopeScripts/dist/pages/special-error/js/index.js @@ -1135,7 +1135,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/build/android/pages/duckplayer/js/index.css b/build/android/pages/duckplayer/js/index.css index de2906b8f..f0299a0ba 100644 --- a/build/android/pages/duckplayer/js/index.css +++ b/build/android/pages/duckplayer/js/index.css @@ -48,7 +48,7 @@ body[data-display=app] { padding: 16px; } -/* pages/duckplayer/app/components/Fallback.module.css */ +/* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; width: 100%; diff --git a/build/android/pages/duckplayer/js/index.js b/build/android/pages/duckplayer/js/index.js index 59dac3b27..7345cb0b5 100644 --- a/build/android/pages/duckplayer/js/index.js +++ b/build/android/pages/duckplayer/js/index.js @@ -1135,7 +1135,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ @@ -2258,12 +2258,12 @@ return q2(UserValuesContext).setEnabled; } - // pages/duckplayer/app/components/Fallback.module.css + // shared/components/Fallback/Fallback.module.css var Fallback_default = { fallback: "Fallback_fallback" }; - // pages/duckplayer/app/components/Fallback.jsx + // shared/components/Fallback/Fallback.jsx function Fallback({ showDetails }) { return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); } diff --git a/build/integration/pages/duckplayer/js/index.css b/build/integration/pages/duckplayer/js/index.css index de2906b8f..f0299a0ba 100644 --- a/build/integration/pages/duckplayer/js/index.css +++ b/build/integration/pages/duckplayer/js/index.css @@ -48,7 +48,7 @@ body[data-display=app] { padding: 16px; } -/* pages/duckplayer/app/components/Fallback.module.css */ +/* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; width: 100%; diff --git a/build/integration/pages/duckplayer/js/index.js b/build/integration/pages/duckplayer/js/index.js index 1e9759385..8d1fc6632 100644 --- a/build/integration/pages/duckplayer/js/index.js +++ b/build/integration/pages/duckplayer/js/index.js @@ -1135,7 +1135,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ @@ -2258,12 +2258,12 @@ return q2(UserValuesContext).setEnabled; } - // pages/duckplayer/app/components/Fallback.module.css + // shared/components/Fallback/Fallback.module.css var Fallback_default = { fallback: "Fallback_fallback" }; - // pages/duckplayer/app/components/Fallback.jsx + // shared/components/Fallback/Fallback.jsx function Fallback({ showDetails }) { return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); } diff --git a/build/integration/pages/example/js/index.js b/build/integration/pages/example/js/index.js index 4b01e58cc..cc27e8d00 100644 --- a/build/integration/pages/example/js/index.js +++ b/build/integration/pages/example/js/index.js @@ -1058,7 +1058,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/build/integration/pages/new-tab/index.html b/build/integration/pages/new-tab/index.html new file mode 100644 index 000000000..6d0102db4 --- /dev/null +++ b/build/integration/pages/new-tab/index.html @@ -0,0 +1,14 @@ + + + + New Tab Page + + + + + + +
+ + + diff --git a/build/integration/pages/new-tab/js/index.css b/build/integration/pages/new-tab/js/index.css new file mode 100644 index 000000000..55225c2b1 --- /dev/null +++ b/build/integration/pages/new-tab/js/index.css @@ -0,0 +1,58 @@ +/* pages/new-tab/app/styles/base.css */ +*, +*:after, +*:before { + box-sizing: border-box; +} +html[data-reduced-motion=true] * { + animation: none !important; + transition: none !important; +} +body { + font-family: system-ui; + margin: 0; + height: 100vh; + width: 100%; + overflow-x: hidden; + user-select: none; + -webkit-user-select: none; + cursor: default; + color: var(--theme-txt-color); + background: var(--theme-page-bg); +} +body > main { + width: 100%; +} +h1, +h2, +h3, +h4 { + margin: 0; +} +button { + font-family: system-ui, sans-serif; +} +ul { + margin: 0; + padding: 0; +} +li { + list-style: none; + margin: 0; + padding: 0; +} + +/* pages/new-tab/app/components/App.module.css */ +.App_layout { + padding-top: var(--sp-16); + padding-bottom: var(--sp-16); + max-width: 504px; + margin-left: auto; + margin-right: auto; +} + +/* shared/components/Fallback/Fallback.module.css */ +.Fallback_fallback { + height: 100%; + width: 100%; +} diff --git a/build/integration/pages/new-tab/js/index.js b/build/integration/pages/new-tab/js/index.js new file mode 100644 index 000000000..6d4317cf0 --- /dev/null +++ b/build/integration/pages/new-tab/js/index.js @@ -0,0 +1,2173 @@ +"use strict"; +(() => { + // ../../node_modules/preact/dist/preact.module.js + var n; + var l; + var u; + var t; + var i; + var o; + var r; + var f; + var e; + var c = {}; + var s = []; + var a = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; + var h = Array.isArray; + function v(n2, l3) { + for (var u3 in l3) + n2[u3] = l3[u3]; + return n2; + } + function p(n2) { + var l3 = n2.parentNode; + l3 && l3.removeChild(n2); + } + function y(l3, u3, t3) { + var i3, o3, r3, f3 = {}; + for (r3 in u3) + "key" == r3 ? i3 = u3[r3] : "ref" == r3 ? o3 = u3[r3] : f3[r3] = u3[r3]; + if (arguments.length > 2 && (f3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), "function" == typeof l3 && null != l3.defaultProps) + for (r3 in l3.defaultProps) + void 0 === f3[r3] && (f3[r3] = l3.defaultProps[r3]); + return d(l3, f3, i3, o3, null); + } + function d(n2, t3, i3, o3, r3) { + var f3 = { type: n2, props: t3, key: i3, ref: o3, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, constructor: void 0, __v: null == r3 ? ++u : r3, __i: -1, __u: 0 }; + return null == r3 && null != l.vnode && l.vnode(f3), f3; + } + function g(n2) { + return n2.children; + } + function b(n2, l3) { + this.props = n2, this.context = l3; + } + function m(n2, l3) { + if (null == l3) + return n2.__ ? m(n2.__, n2.__i + 1) : null; + for (var u3; l3 < n2.__k.length; l3++) + if (null != (u3 = n2.__k[l3]) && null != u3.__e) + return u3.__e; + return "function" == typeof n2.type ? m(n2) : null; + } + function k(n2) { + var l3, u3; + if (null != (n2 = n2.__) && null != n2.__c) { + for (n2.__e = n2.__c.base = null, l3 = 0; l3 < n2.__k.length; l3++) + if (null != (u3 = n2.__k[l3]) && null != u3.__e) { + n2.__e = n2.__c.base = u3.__e; + break; + } + return k(n2); + } + } + function w(n2) { + (!n2.__d && (n2.__d = true) && i.push(n2) && !x.__r++ || o !== l.debounceRendering) && ((o = l.debounceRendering) || r)(x); + } + function x() { + var n2, u3, t3, o3, r3, e3, c3, s3, a3; + for (i.sort(f); n2 = i.shift(); ) + n2.__d && (u3 = i.length, o3 = void 0, e3 = (r3 = (t3 = n2).__v).__e, s3 = [], a3 = [], (c3 = t3.__P) && ((o3 = v({}, r3)).__v = r3.__v + 1, l.vnode && l.vnode(o3), L(c3, o3, r3, t3.__n, void 0 !== c3.ownerSVGElement, 32 & r3.__u ? [e3] : null, s3, null == e3 ? m(r3) : e3, !!(32 & r3.__u), a3), o3.__.__k[o3.__i] = o3, M(s3, o3, a3), o3.__e != e3 && k(o3)), i.length > u3 && i.sort(f)); + x.__r = 0; + } + function C(n2, l3, u3, t3, i3, o3, r3, f3, e3, a3, h3) { + var v3, p3, y2, d3, _2, g3 = t3 && t3.__k || s, b3 = l3.length; + for (u3.__d = e3, P(u3, l3, g3), e3 = u3.__d, v3 = 0; v3 < b3; v3++) + null != (y2 = u3.__k[v3]) && "boolean" != typeof y2 && "function" != typeof y2 && (p3 = -1 === y2.__i ? c : g3[y2.__i] || c, y2.__i = v3, L(n2, y2, p3, i3, o3, r3, f3, e3, a3, h3), d3 = y2.__e, y2.ref && p3.ref != y2.ref && (p3.ref && z(p3.ref, null, y2), h3.push(y2.ref, y2.__c || d3, y2)), null == _2 && null != d3 && (_2 = d3), 65536 & y2.__u || p3.__k === y2.__k ? e3 = S(y2, e3, n2) : "function" == typeof y2.type && void 0 !== y2.__d ? e3 = y2.__d : d3 && (e3 = d3.nextSibling), y2.__d = void 0, y2.__u &= -196609); + u3.__d = e3, u3.__e = _2; + } + function P(n2, l3, u3) { + var t3, i3, o3, r3, f3, e3 = l3.length, c3 = u3.length, s3 = c3, a3 = 0; + for (n2.__k = [], t3 = 0; t3 < e3; t3++) + null != (i3 = n2.__k[t3] = null == (i3 = l3[t3]) || "boolean" == typeof i3 || "function" == typeof i3 ? null : "string" == typeof i3 || "number" == typeof i3 || "bigint" == typeof i3 || i3.constructor == String ? d(null, i3, null, null, i3) : h(i3) ? d(g, { children: i3 }, null, null, null) : void 0 === i3.constructor && i3.__b > 0 ? d(i3.type, i3.props, i3.key, i3.ref ? i3.ref : null, i3.__v) : i3) ? (i3.__ = n2, i3.__b = n2.__b + 1, f3 = H(i3, u3, r3 = t3 + a3, s3), i3.__i = f3, o3 = null, -1 !== f3 && (s3--, (o3 = u3[f3]) && (o3.__u |= 131072)), null == o3 || null === o3.__v ? (-1 == f3 && a3--, "function" != typeof i3.type && (i3.__u |= 65536)) : f3 !== r3 && (f3 === r3 + 1 ? a3++ : f3 > r3 ? s3 > e3 - r3 ? a3 += f3 - r3 : a3-- : a3 = f3 < r3 && f3 == r3 - 1 ? f3 - r3 : 0, f3 !== t3 + a3 && (i3.__u |= 65536))) : (o3 = u3[t3]) && null == o3.key && o3.__e && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3, false), u3[t3] = null, s3--); + if (s3) + for (t3 = 0; t3 < c3; t3++) + null != (o3 = u3[t3]) && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3)); + } + function S(n2, l3, u3) { + var t3, i3; + if ("function" == typeof n2.type) { + for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++) + t3[i3] && (t3[i3].__ = n2, l3 = S(t3[i3], l3, u3)); + return l3; + } + return n2.__e != l3 && (u3.insertBefore(n2.__e, l3 || null), l3 = n2.__e), l3 && l3.nextSibling; + } + function H(n2, l3, u3, t3) { + var i3 = n2.key, o3 = n2.type, r3 = u3 - 1, f3 = u3 + 1, e3 = l3[u3]; + if (null === e3 || e3 && i3 == e3.key && o3 === e3.type) + return u3; + if (t3 > (null != e3 && 0 == (131072 & e3.__u) ? 1 : 0)) + for (; r3 >= 0 || f3 < l3.length; ) { + if (r3 >= 0) { + if ((e3 = l3[r3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type) + return r3; + r3--; + } + if (f3 < l3.length) { + if ((e3 = l3[f3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type) + return f3; + f3++; + } + } + return -1; + } + function I(n2, l3, u3) { + "-" === l3[0] ? n2.setProperty(l3, null == u3 ? "" : u3) : n2[l3] = null == u3 ? "" : "number" != typeof u3 || a.test(l3) ? u3 : u3 + "px"; + } + function T(n2, l3, u3, t3, i3) { + var o3; + n: + if ("style" === l3) + if ("string" == typeof u3) + n2.style.cssText = u3; + else { + if ("string" == typeof t3 && (n2.style.cssText = t3 = ""), t3) + for (l3 in t3) + u3 && l3 in u3 || I(n2.style, l3, ""); + if (u3) + for (l3 in u3) + t3 && u3[l3] === t3[l3] || I(n2.style, l3, u3[l3]); + } + else if ("o" === l3[0] && "n" === l3[1]) + o3 = l3 !== (l3 = l3.replace(/(PointerCapture)$|Capture$/, "$1")), l3 = l3.toLowerCase() in n2 ? l3.toLowerCase().slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + o3] = u3, u3 ? t3 ? u3.u = t3.u : (u3.u = Date.now(), n2.addEventListener(l3, o3 ? D : A, o3)) : n2.removeEventListener(l3, o3 ? D : A, o3); + else { + if (i3) + l3 = l3.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s"); + else if ("width" !== l3 && "height" !== l3 && "href" !== l3 && "list" !== l3 && "form" !== l3 && "tabIndex" !== l3 && "download" !== l3 && "rowSpan" !== l3 && "colSpan" !== l3 && "role" !== l3 && l3 in n2) + try { + n2[l3] = null == u3 ? "" : u3; + break n; + } catch (n3) { + } + "function" == typeof u3 || (null == u3 || false === u3 && "-" !== l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, u3)); + } + } + function A(n2) { + var u3 = this.l[n2.type + false]; + if (n2.t) { + if (n2.t <= u3.u) + return; + } else + n2.t = Date.now(); + return u3(l.event ? l.event(n2) : n2); + } + function D(n2) { + return this.l[n2.type + true](l.event ? l.event(n2) : n2); + } + function L(n2, u3, t3, i3, o3, r3, f3, e3, c3, s3) { + var a3, p3, y2, d3, _2, m3, k3, w3, x2, P2, S2, $, H2, I2, T3, A2 = u3.type; + if (void 0 !== u3.constructor) + return null; + 128 & t3.__u && (c3 = !!(32 & t3.__u), r3 = [e3 = u3.__e = t3.__e]), (a3 = l.__b) && a3(u3); + n: + if ("function" == typeof A2) + try { + if (w3 = u3.props, x2 = (a3 = A2.contextType) && i3[a3.__c], P2 = a3 ? x2 ? x2.props.value : a3.__ : i3, t3.__c ? k3 = (p3 = u3.__c = t3.__c).__ = p3.__E : ("prototype" in A2 && A2.prototype.render ? u3.__c = p3 = new A2(w3, P2) : (u3.__c = p3 = new b(w3, P2), p3.constructor = A2, p3.render = O), x2 && x2.sub(p3), p3.props = w3, p3.state || (p3.state = {}), p3.context = P2, p3.__n = i3, y2 = p3.__d = true, p3.__h = [], p3._sb = []), null == p3.__s && (p3.__s = p3.state), null != A2.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = v({}, p3.__s)), v(p3.__s, A2.getDerivedStateFromProps(w3, p3.__s))), d3 = p3.props, _2 = p3.state, p3.__v = u3, y2) + null == A2.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), null != p3.componentDidMount && p3.__h.push(p3.componentDidMount); + else { + if (null == A2.getDerivedStateFromProps && w3 !== d3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(w3, P2), !p3.__e && (null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(w3, p3.__s, P2) || u3.__v === t3.__v)) { + for (u3.__v !== t3.__v && (p3.props = w3, p3.state = p3.__s, p3.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.forEach(function(n3) { + n3 && (n3.__ = u3); + }), S2 = 0; S2 < p3._sb.length; S2++) + p3.__h.push(p3._sb[S2]); + p3._sb = [], p3.__h.length && f3.push(p3); + break n; + } + null != p3.componentWillUpdate && p3.componentWillUpdate(w3, p3.__s, P2), null != p3.componentDidUpdate && p3.__h.push(function() { + p3.componentDidUpdate(d3, _2, m3); + }); + } + if (p3.context = P2, p3.props = w3, p3.__P = n2, p3.__e = false, $ = l.__r, H2 = 0, "prototype" in A2 && A2.prototype.render) { + for (p3.state = p3.__s, p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), I2 = 0; I2 < p3._sb.length; I2++) + p3.__h.push(p3._sb[I2]); + p3._sb = []; + } else + do { + p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s; + } while (p3.__d && ++H2 < 25); + p3.state = p3.__s, null != p3.getChildContext && (i3 = v(v({}, i3), p3.getChildContext())), y2 || null == p3.getSnapshotBeforeUpdate || (m3 = p3.getSnapshotBeforeUpdate(d3, _2)), C(n2, h(T3 = null != a3 && a3.type === g && null == a3.key ? a3.props.children : a3) ? T3 : [T3], u3, t3, i3, o3, r3, f3, e3, c3, s3), p3.base = u3.__e, u3.__u &= -161, p3.__h.length && f3.push(p3), k3 && (p3.__E = p3.__ = null); + } catch (n3) { + u3.__v = null, c3 || null != r3 ? (u3.__e = e3, u3.__u |= c3 ? 160 : 32, r3[r3.indexOf(e3)] = null) : (u3.__e = t3.__e, u3.__k = t3.__k), l.__e(n3, u3, t3); + } + else + null == r3 && u3.__v === t3.__v ? (u3.__k = t3.__k, u3.__e = t3.__e) : u3.__e = j(t3.__e, u3, t3, i3, o3, r3, f3, c3, s3); + (a3 = l.diffed) && a3(u3); + } + function M(n2, u3, t3) { + u3.__d = void 0; + for (var i3 = 0; i3 < t3.length; i3++) + z(t3[i3], t3[++i3], t3[++i3]); + l.__c && l.__c(u3, n2), n2.some(function(u4) { + try { + n2 = u4.__h, u4.__h = [], n2.some(function(n3) { + n3.call(u4); + }); + } catch (n3) { + l.__e(n3, u4.__v); + } + }); + } + function j(l3, u3, t3, i3, o3, r3, f3, e3, s3) { + var a3, v3, y2, d3, _2, g3, b3, k3 = t3.props, w3 = u3.props, x2 = u3.type; + if ("svg" === x2 && (o3 = true), null != r3) { + for (a3 = 0; a3 < r3.length; a3++) + if ((_2 = r3[a3]) && "setAttribute" in _2 == !!x2 && (x2 ? _2.localName === x2 : 3 === _2.nodeType)) { + l3 = _2, r3[a3] = null; + break; + } + } + if (null == l3) { + if (null === x2) + return document.createTextNode(w3); + l3 = o3 ? document.createElementNS("http://www.w3.org/2000/svg", x2) : document.createElement(x2, w3.is && w3), r3 = null, e3 = false; + } + if (null === x2) + k3 === w3 || e3 && l3.data === w3 || (l3.data = w3); + else { + if (r3 = r3 && n.call(l3.childNodes), k3 = t3.props || c, !e3 && null != r3) + for (k3 = {}, a3 = 0; a3 < l3.attributes.length; a3++) + k3[(_2 = l3.attributes[a3]).name] = _2.value; + for (a3 in k3) + _2 = k3[a3], "children" == a3 || ("dangerouslySetInnerHTML" == a3 ? y2 = _2 : "key" === a3 || a3 in w3 || T(l3, a3, null, _2, o3)); + for (a3 in w3) + _2 = w3[a3], "children" == a3 ? d3 = _2 : "dangerouslySetInnerHTML" == a3 ? v3 = _2 : "value" == a3 ? g3 = _2 : "checked" == a3 ? b3 = _2 : "key" === a3 || e3 && "function" != typeof _2 || k3[a3] === _2 || T(l3, a3, _2, k3[a3], o3); + if (v3) + e3 || y2 && (v3.__html === y2.__html || v3.__html === l3.innerHTML) || (l3.innerHTML = v3.__html), u3.__k = []; + else if (y2 && (l3.innerHTML = ""), C(l3, h(d3) ? d3 : [d3], u3, t3, i3, o3 && "foreignObject" !== x2, r3, f3, r3 ? r3[0] : t3.__k && m(t3, 0), e3, s3), null != r3) + for (a3 = r3.length; a3--; ) + null != r3[a3] && p(r3[a3]); + e3 || (a3 = "value", void 0 !== g3 && (g3 !== l3[a3] || "progress" === x2 && !g3 || "option" === x2 && g3 !== k3[a3]) && T(l3, a3, g3, k3[a3], false), a3 = "checked", void 0 !== b3 && b3 !== l3[a3] && T(l3, a3, b3, k3[a3], false)); + } + return l3; + } + function z(n2, u3, t3) { + try { + "function" == typeof n2 ? n2(u3) : n2.current = u3; + } catch (n3) { + l.__e(n3, t3); + } + } + function N(n2, u3, t3) { + var i3, o3; + if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current !== n2.__e || z(i3, null, u3)), null != (i3 = n2.__c)) { + if (i3.componentWillUnmount) + try { + i3.componentWillUnmount(); + } catch (n3) { + l.__e(n3, u3); + } + i3.base = i3.__P = null, n2.__c = void 0; + } + if (i3 = n2.__k) + for (o3 = 0; o3 < i3.length; o3++) + i3[o3] && N(i3[o3], u3, t3 || "function" != typeof n2.type); + t3 || null == n2.__e || p(n2.__e), n2.__ = n2.__e = n2.__d = void 0; + } + function O(n2, l3, u3) { + return this.constructor(n2, u3); + } + function q(u3, t3, i3) { + var o3, r3, f3, e3; + l.__ && l.__(u3, t3), r3 = (o3 = "function" == typeof i3) ? null : i3 && i3.__k || t3.__k, f3 = [], e3 = [], L(t3, u3 = (!o3 && i3 || t3).__k = y(g, null, [u3]), r3 || c, c, void 0 !== t3.ownerSVGElement, !o3 && i3 ? [i3] : r3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, f3, !o3 && i3 ? i3 : r3 ? r3.__e : t3.firstChild, o3, e3), M(f3, u3, e3); + } + function F(n2, l3) { + var u3 = { __c: l3 = "__cC" + e++, __: n2, Consumer: function(n3, l4) { + return n3.children(l4); + }, Provider: function(n3) { + var u4, t3; + return this.getChildContext || (u4 = [], (t3 = {})[l3] = this, this.getChildContext = function() { + return t3; + }, this.shouldComponentUpdate = function(n4) { + this.props.value !== n4.value && u4.some(function(n5) { + n5.__e = true, w(n5); + }); + }, this.sub = function(n4) { + u4.push(n4); + var l4 = n4.componentWillUnmount; + n4.componentWillUnmount = function() { + u4.splice(u4.indexOf(n4), 1), l4 && l4.call(n4); + }; + }), n3.children; + } }; + return u3.Provider.__ = u3.Consumer.contextType = u3; + } + n = s.slice, l = { __e: function(n2, l3, u3, t3) { + for (var i3, o3, r3; l3 = l3.__; ) + if ((i3 = l3.__c) && !i3.__) + try { + if ((o3 = i3.constructor) && null != o3.getDerivedStateFromError && (i3.setState(o3.getDerivedStateFromError(n2)), r3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), r3 = i3.__d), r3) + return i3.__E = i3; + } catch (l4) { + n2 = l4; + } + throw n2; + } }, u = 0, t = function(n2) { + return null != n2 && null == n2.constructor; + }, b.prototype.setState = function(n2, l3) { + var u3; + u3 = null != this.__s && this.__s !== this.state ? this.__s : this.__s = v({}, this.state), "function" == typeof n2 && (n2 = n2(v({}, u3), this.props)), n2 && v(u3, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), w(this)); + }, b.prototype.forceUpdate = function(n2) { + this.__v && (this.__e = true, n2 && this.__h.push(n2), w(this)); + }, b.prototype.render = g, i = [], r = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, f = function(n2, l3) { + return n2.__v.__b - l3.__v.__b; + }, x.__r = 0, e = 0; + + // ../../node_modules/preact/devtools/dist/devtools.module.js + "undefined" != typeof window && window.__PREACT_DEVTOOLS__ && window.__PREACT_DEVTOOLS__.attachPreact("10.19.3", l, { Fragment: g, Component: b }); + + // pages/new-tab/app/components/App.module.css + var App_default = { + layout: "App_layout" + }; + + // pages/new-tab/app/components/App.js + function App({ children }) { + return /* @__PURE__ */ y("div", { className: App_default.layout }, children); + } + + // ../../node_modules/preact/hooks/dist/hooks.module.js + var t2; + var r2; + var u2; + var i2; + var o2 = 0; + var f2 = []; + var c2 = []; + var e2 = l.__b; + var a2 = l.__r; + var v2 = l.diffed; + var l2 = l.__c; + var m2 = l.unmount; + function d2(t3, u3) { + l.__h && l.__h(r2, t3, o2 || u3), o2 = 0; + var i3 = r2.__H || (r2.__H = { __: [], __h: [] }); + return t3 >= i3.__.length && i3.__.push({ __V: c2 }), i3.__[t3]; + } + function h2(n2) { + return o2 = 1, s2(B, n2); + } + function s2(n2, u3, i3) { + var o3 = d2(t2++, 2); + if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u3) : B(void 0, u3), function(n3) { + var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3); + t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({})); + }], o3.__c = r2, !r2.u)) { + var f3 = function(n3, t3, r3) { + if (!o3.__c.__H) + return true; + var u4 = o3.__c.__H.__.filter(function(n4) { + return n4.__c; + }); + if (u4.every(function(n4) { + return !n4.__N; + })) + return !c3 || c3.call(this, n3, t3, r3); + var i4 = false; + return u4.forEach(function(n4) { + if (n4.__N) { + var t4 = n4.__[0]; + n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true); + } + }), !(!i4 && o3.__c.props === n3) && (!c3 || c3.call(this, n3, t3, r3)); + }; + r2.u = true; + var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate; + r2.componentWillUpdate = function(n3, t3, r3) { + if (this.__e) { + var u4 = c3; + c3 = void 0, f3(n3, t3, r3), c3 = u4; + } + e3 && e3.call(this, n3, t3, r3); + }, r2.shouldComponentUpdate = f3; + } + return o3.__N || o3.__; + } + function p2(u3, i3) { + var o3 = d2(t2++, 3); + !l.__s && z2(o3.__H, i3) && (o3.__ = u3, o3.i = i3, r2.__H.__h.push(o3)); + } + function F2(n2, r3) { + var u3 = d2(t2++, 7); + return z2(u3.__H, r3) ? (u3.__V = n2(), u3.i = r3, u3.__h = n2, u3.__V) : u3.__; + } + function T2(n2, t3) { + return o2 = 8, F2(function() { + return n2; + }, t3); + } + function q2(n2) { + var u3 = r2.context[n2.__c], i3 = d2(t2++, 9); + return i3.c = n2, u3 ? (null == i3.__ && (i3.__ = true, u3.sub(r2)), u3.props.value) : n2.__; + } + function b2() { + for (var t3; t3 = f2.shift(); ) + if (t3.__P && t3.__H) + try { + t3.__H.__h.forEach(k2), t3.__H.__h.forEach(w2), t3.__H.__h = []; + } catch (r3) { + t3.__H.__h = [], l.__e(r3, t3.__v); + } + } + l.__b = function(n2) { + r2 = null, e2 && e2(n2); + }, l.__r = function(n2) { + a2 && a2(n2), t2 = 0; + var i3 = (r2 = n2.__c).__H; + i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.forEach(function(n3) { + n3.__N && (n3.__ = n3.__N), n3.__V = c2, n3.__N = n3.i = void 0; + })) : (i3.__h.forEach(k2), i3.__h.forEach(w2), i3.__h = [], t2 = 0)), u2 = r2; + }, l.diffed = function(t3) { + v2 && v2(t3); + var o3 = t3.__c; + o3 && o3.__H && (o3.__H.__h.length && (1 !== f2.push(o3) && i2 === l.requestAnimationFrame || ((i2 = l.requestAnimationFrame) || j2)(b2)), o3.__H.__.forEach(function(n2) { + n2.i && (n2.__H = n2.i), n2.__V !== c2 && (n2.__ = n2.__V), n2.i = void 0, n2.__V = c2; + })), u2 = r2 = null; + }, l.__c = function(t3, r3) { + r3.some(function(t4) { + try { + t4.__h.forEach(k2), t4.__h = t4.__h.filter(function(n2) { + return !n2.__ || w2(n2); + }); + } catch (u3) { + r3.some(function(n2) { + n2.__h && (n2.__h = []); + }), r3 = [], l.__e(u3, t4.__v); + } + }), l2 && l2(t3, r3); + }, l.unmount = function(t3) { + m2 && m2(t3); + var r3, u3 = t3.__c; + u3 && u3.__H && (u3.__H.__.forEach(function(n2) { + try { + k2(n2); + } catch (n3) { + r3 = n3; + } + }), u3.__H = void 0, r3 && l.__e(r3, u3.__v)); + }; + var g2 = "function" == typeof requestAnimationFrame; + function j2(n2) { + var t3, r3 = function() { + clearTimeout(u3), g2 && cancelAnimationFrame(t3), setTimeout(n2); + }, u3 = setTimeout(r3, 100); + g2 && (t3 = requestAnimationFrame(r3)); + } + function k2(n2) { + var t3 = r2, u3 = n2.__c; + "function" == typeof u3 && (n2.__c = void 0, u3()), r2 = t3; + } + function w2(n2) { + var t3 = r2; + n2.__c = n2.__(), r2 = t3; + } + function z2(n2, t3) { + return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) { + return t4 !== n2[r3]; + }); + } + function B(n2, t3) { + return "function" == typeof t3 ? t3(n2) : t3; + } + + // shared/components/EnvironmentProvider.js + var EnvironmentContext = F({ + isReducedMotion: false, + isDarkMode: false, + debugState: false, + injectName: ( + /** @type {import('../environment').Environment['injectName']} */ + "windows" + ), + willThrow: false + }); + var THEME_QUERY = "(prefers-color-scheme: dark)"; + var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)"; + function EnvironmentProvider({ children, debugState, willThrow = false, injectName = "windows" }) { + const [theme, setTheme] = h2(window.matchMedia(THEME_QUERY).matches ? "dark" : "light"); + const [isReducedMotion, setReducedMotion] = h2(window.matchMedia(REDUCED_MOTION_QUERY).matches); + p2(() => { + const mediaQueryList = window.matchMedia(THEME_QUERY); + const listener = (e3) => setTheme(e3.matches ? "dark" : "light"); + mediaQueryList.addEventListener("change", listener); + return () => mediaQueryList.removeEventListener("change", listener); + }, []); + p2(() => { + const mediaQueryList = window.matchMedia(REDUCED_MOTION_QUERY); + const listener = (e3) => setter(e3.matches); + mediaQueryList.addEventListener("change", listener); + setter(mediaQueryList.matches); + function setter(value) { + document.documentElement.dataset.reducedMotion = String(value); + setReducedMotion(value); + } + window.addEventListener("toggle-reduced-motion", () => { + setter(true); + }); + return () => mediaQueryList.removeEventListener("change", listener); + }, []); + return /* @__PURE__ */ y(EnvironmentContext.Provider, { value: { + isReducedMotion, + debugState, + isDarkMode: theme === "dark", + injectName, + willThrow + } }, children); + } + function UpdateEnvironment({ search }) { + p2(() => { + const params = new URLSearchParams(search); + if (params.has("reduced-motion")) { + setTimeout(() => { + window.dispatchEvent(new CustomEvent("toggle-reduced-motion")); + }, 0); + } + }, [search]); + return null; + } + + // shared/components/Fallback/Fallback.module.css + var Fallback_default = { + fallback: "Fallback_fallback" + }; + + // shared/components/Fallback/Fallback.jsx + function Fallback({ showDetails }) { + return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); + } + + // shared/components/ErrorBoundary.js + var ErrorBoundary = class extends b { + /** + * @param {{didCatch: (params: {error: Error; info: any}) => void}} props + */ + constructor(props) { + super(props); + this.state = { hasError: false }; + } + static getDerivedStateFromError() { + return { hasError: true }; + } + componentDidCatch(error, info) { + console.error(error); + console.log(info); + this.props.didCatch({ error, info }); + } + render() { + if (this.state.hasError) { + return this.props.fallback; + } + return this.props.children; + } + }; + + // pages/new-tab/app/settings.provider.js + var SettingsContext = F( + /** @type {{settings: import("./settings.js").Settings}} */ + {} + ); + function SettingsProvider({ settings, children }) { + return /* @__PURE__ */ y(SettingsContext.Provider, { value: { settings } }, children); + } + + // shared/translations.js + function apply(subject, replacements, textLength = 1) { + if (typeof subject !== "string" || subject.length === 0) + return ""; + let out = subject; + if (replacements) { + for (let [name, value] of Object.entries(replacements)) { + if (typeof value !== "string") + value = ""; + out = out.replaceAll(`{${name}}`, value); + } + } + if (textLength !== 1 && textLength > 0 && textLength <= 2) { + const targetLen = Math.ceil(out.length * textLength); + const target = Math.ceil(textLength); + const combined = out.repeat(target); + return combined.slice(0, targetLen); + } + return out; + } + + // shared/components/TranslationsProvider.js + var TranslationContext = F({ + /** @type {LocalTranslationFn} */ + t: () => { + throw new Error("must implement"); + } + }); + function TranslationProvider({ children, translationObject, fallback, textLength = 1 }) { + function t3(inputKey, replacements) { + const subject = translationObject?.[inputKey]?.title || fallback?.[inputKey]?.title; + return apply(subject, replacements, textLength); + } + return /* @__PURE__ */ y(TranslationContext.Provider, { value: { t: t3 } }, children); + } + + // pages/new-tab/src/locales/en/newtab.json + var newtab_default = { + smartling: { + string_format: "icu", + translate_paths: [ + { + path: "*/title", + key: "{*}/title", + instruction: "*/note" + } + ] + }, + helloWorld: { + title: "Hello world!", + note: "here!" + } + }; + + // pages/new-tab/app/types.js + var MessagingContext = F( + /** @type {import("../src/js/index.js").NewTabPage} */ + {} + ); + + // pages/new-tab/app/widget-list/widget-config.js + var WidgetConfigAPI = class { + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {WidgetConfig} initialData - Initial widget configuration. + */ + constructor(ntp, initialData) { + this.ntp = ntp; + this.manager = new WidgetConfigManager(ntp, initialData); + } + /** + * @param {(data: WidgetConfig) => void} cb + * @return {() => void} - call this returned method to dispose of the subscription + */ + onUpdate(cb) { + const controller = new AbortController(); + this.manager.eventTarget.addEventListener(this.manager.DATA_CHANGE_EVT, (evt) => { + cb(evt.detail); + }, { signal: controller.signal }); + return () => controller.abort(); + } + /** + * @param {string} id + */ + show(id) { + const next = this.manager.inMemoryData.map((w3) => { + if (w3.id === id) + return { ...w3, visibility: ( + /** @type {const} */ + "visible" + ) }; + return w3; + }); + this.manager.update(next); + } + /** + * @param {string} id + */ + hide(id) { + const next = this.manager.inMemoryData.map((w3) => { + if (w3.id === id) + return { ...w3, visibility: ( + /** @type {const} */ + "hidden" + ) }; + return w3; + }); + this.manager.update(next); + } + }; + var WidgetConfigManager = class { + debounceTimer = null; + eventTarget = new EventTarget(); + DATA_CHANGE_EVT = "dataChanged"; + DEBOUNCE_TIME_MS = 200; + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {WidgetConfig} initialData - Initial widget configuration. + */ + constructor(ntp, initialData) { + this.ntp = ntp; + this.inMemoryData = initialData; + this.setupSubscriptionStream(); + } + /** + * Sets up the subscription stream from the data feed, which behaves like an input source + * that updates in-memory data but does not trigger persistence. + */ + setupSubscriptionStream() { + this.ntp.messaging.subscribe("onWidgetConfigUpdated", (newData) => { + this.updateInMemoryData(newData.widgetConfig, "subscription"); + }); + } + /** + * Manually trigger an update, for example, from a UI element. + * @param {WidgetConfig} newData - The new widget configuration to update. + */ + update(newData) { + this.updateInMemoryData(newData, "manual"); + } + /** + * Updates the in-memory data and triggers persistence if the source is a manual update. + * This method centralizes all state updates. + * @param {WidgetConfig} newData - The new widget configuration to update in memory. + * @param {'manual' | 'subscription'} source - The source of the update. Either 'subscription' or 'manual'. + */ + updateInMemoryData(newData, source) { + this.log(`Updating in-memory data from '${source}:'`, newData); + this.inMemoryData = structuredClone(newData); + this.broadcastChange(); + if (source === "manual") { + this.clearDebounceTimer(); + this.debounceTimer = /** @type {any} */ + setTimeout(() => { + this.persist(); + }, this.DEBOUNCE_TIME_MS); + } + } + /** + * Clears the debounce timer if it exists, simulating the switchMap behavior. + */ + clearDebounceTimer() { + if (this.debounceTimer) { + this.log("Clearing previous debounce timer."); + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + } + } + /** + * Broadcasts the current state to external subscribers using EventTarget. + */ + broadcastChange() { + this.log("Broadcasting change to external listeners:", this.inMemoryData); + this.eventTarget.dispatchEvent(new CustomEvent(this.DATA_CHANGE_EVT, { detail: this.inMemoryData })); + } + /** + * Persists the current in-memory widget configuration state to the internal data feed. + */ + persist() { + this.log("will persist data to backend:", this.inMemoryData); + this.ntp.messaging.notify("setWidgetConfig", { widgetConfig: this.inMemoryData }); + } + /** + * Logs messages to the console for tracing internal state updates. + * @param {string} message - The message to log. + * @param {any} data - The associated data for the message. + */ + log(message, data = null) { + console.log(`[WidgetConfigManager] ${message}`, data); + } + }; + + // pages/new-tab/app/widget-list/widget-config.provider.js + var WidgetConfigContext = F({ + /** @type {WidgetList} */ + widgets: [], + /** @type {WidgetConfig} */ + widgetConfig: [], + /** @type {(name:string) => void} */ + hide: () => { + }, + /** @type {(name:string) => void} */ + show: () => { + } + }); + var WidgetConfigDispatchContext = F({ + dispatch: null + }); + function WidgetConfigProvider(props) { + const [data, setData] = h2(props.widgetConfig); + p2(() => { + const unsub = props.api.onUpdate((widgetConfig) => { + setData(widgetConfig); + }); + return () => unsub(); + }, [props.api]); + function hide(name) { + console.log("will hide", name); + props.api.hide(name); + } + function show(name) { + console.log("will show", name); + props.api.show(name); + } + return /* @__PURE__ */ y(WidgetConfigContext.Provider, { value: { + // this field is static for the lifespan of the page + widgets: props.widgets, + // this will be updated via subscriptions + widgetConfig: data, + hide, + show + } }, props.children); + } + var WidgetVisibilityContext = F({ + visibility: ( + /** @type {WidgetConfigItem['visibility']} */ + "visible" + ), + id: ( + /** @type {WidgetConfigItem['id']} */ + "" + ), + toggle: () => { + } + }); + function useVisibility() { + return q2(WidgetVisibilityContext); + } + function WidgetVisibilityProvider(props) { + const { widgetConfig, show, hide } = q2(WidgetConfigContext); + const toggle = T2(() => { + const matching = widgetConfig.find((x2) => x2.id === props.id); + if (matching?.visibility === "visible") { + hide(props.id); + } else { + show(props.id); + } + }, [props.id, widgetConfig]); + return /* @__PURE__ */ y(WidgetVisibilityContext.Provider, { value: { + visibility: props.visibility, + id: props.id, + toggle + } }, props.children); + } + + // pages/new-tab/app/widget-list/WidgetList.js + var widgetMap = { + favorites: () => /* @__PURE__ */ y(Favorites, null), + privacyStats: () => /* @__PURE__ */ y(PrivacyStats, null) + }; + function WidgetList() { + const { widgets, widgetConfig } = q2(WidgetConfigContext); + return /* @__PURE__ */ y("div", null, widgets.map((widget) => { + const matchingConfig = widgetConfig.find((item) => item.id === widget.id); + if (!matchingConfig) { + console.warn("missing config for widget: ", widget); + return null; + } + return /* @__PURE__ */ y(g, { key: widget.id }, /* @__PURE__ */ y( + WidgetVisibilityProvider, + { + visibility: matchingConfig.visibility, + id: matchingConfig.id + }, + widgetMap[widget.id]?.() + )); + })); + } + function Favorites() { + const { visibility, id, toggle } = useVisibility(); + return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Favorites Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Favorites")); + } + function PrivacyStats() { + const { visibility, id, toggle } = useVisibility(); + return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Privacy Stats Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Privacy Stats")); + } + + // pages/new-tab/app/settings.js + var Settings = class _Settings { + /** + * @param {object} params + * @param {{name: ImportMeta['platform']}} [params.platform] + */ + constructor({ + platform = { name: "windows" } + }) { + this.platform = platform; + } + withPlatformName(name) { + const valid = ["windows", "macos", "ios", "android"]; + if (valid.includes( + /** @type {any} */ + name + )) { + return new _Settings({ + ...this, + platform: { name } + }); + } + return this; + } + }; + + // pages/new-tab/app/index.js + async function init(messaging2, baseEnvironment2) { + const init2 = await messaging2.init(); + if (!Array.isArray(init2.widgets)) { + throw new Error("missing critical initialSetup.widgets array"); + } + if (!Array.isArray(init2.widgetConfig)) { + throw new Error("missing critical initialSetup.widgetConfig array"); + } + const widgetConfigAPI = new WidgetConfigAPI(messaging2, init2.widgetConfig); + const environment = baseEnvironment2.withEnv(init2.env).withLocale(init2.locale).withLocale(baseEnvironment2.urlParams.get("locale")).withTextLength(baseEnvironment2.urlParams.get("textLength")).withDisplay(baseEnvironment2.urlParams.get("display")); + console.log("environment:", environment); + console.log("locale:", environment.locale); + const strings = environment.locale === "en" ? newtab_default : await fetch(`./locales/${environment.locale}/new-tab.json`).then((x2) => x2.json()).catch((e3) => { + console.error("Could not load locale", environment.locale, e3); + return newtab_default; + }); + const settings = new Settings({}).withPlatformName(baseEnvironment2.injectName).withPlatformName(init2.platform?.name).withPlatformName(baseEnvironment2.urlParams.get("platform")); + const didCatch = (error) => { + const message = error?.message || error?.error || "unknown"; + messaging2.reportPageException({ message }); + }; + const root = document.querySelector("#app"); + if (!root) + throw new Error("could not render, root element missing"); + q( + /* @__PURE__ */ y( + EnvironmentProvider, + { + debugState: environment.debugState, + injectName: environment.injectName, + willThrow: environment.willThrow + }, + /* @__PURE__ */ y(ErrorBoundary, { didCatch, fallback: /* @__PURE__ */ y(Fallback, { showDetails: environment.env === "development" }) }, /* @__PURE__ */ y(UpdateEnvironment, { search: window.location.search }), /* @__PURE__ */ y(MessagingContext.Provider, { value: messaging2 }, /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(TranslationProvider, { translationObject: strings, fallback: strings, textLength: environment.textLength }, /* @__PURE__ */ y(WidgetConfigProvider, { api: widgetConfigAPI, widgetConfig: init2.widgetConfig, widgets: init2.widgets }, /* @__PURE__ */ y(App, null, /* @__PURE__ */ y(WidgetList, null))))))) + ), + root + ); + } + + // ../messaging/lib/windows.js + var WindowsMessagingTransport = class { + /** + * @param {WindowsMessagingConfig} config + * @param {import('../index.js').MessagingContext} messagingContext + * @internal + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + this.globals = { + window, + JSONparse: window.JSON.parse, + JSONstringify: window.JSON.stringify, + Promise: window.Promise, + Error: window.Error, + String: window.String + }; + for (const [methodName, fn] of Object.entries(this.config.methods)) { + if (typeof fn !== "function") { + throw new Error("cannot create WindowsMessagingTransport, missing the method: " + methodName); + } + } + } + /** + * @param {import('../index.js').NotificationMessage} msg + */ + notify(msg) { + const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {})); + const notification = WindowsNotification.fromNotification(msg, data); + this.config.methods.postMessage(notification); + } + /** + * @param {import('../index.js').RequestMessage} msg + * @param {{signal?: AbortSignal}} opts + * @return {Promise} + */ + request(msg, opts = {}) { + const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {})); + const outgoing = WindowsRequestMessage.fromRequest(msg, data); + this.config.methods.postMessage(outgoing); + const comparator = (eventData) => { + return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.id === msg.id; + }; + function isMessageResponse(data2) { + if ("result" in data2) + return true; + if ("error" in data2) + return true; + return false; + } + return new this.globals.Promise((resolve, reject) => { + try { + this._subscribe(comparator, opts, (value, unsubscribe) => { + unsubscribe(); + if (!isMessageResponse(value)) { + console.warn("unknown response type", value); + return reject(new this.globals.Error("unknown response")); + } + if (value.result) { + return resolve(value.result); + } + const message = this.globals.String(value.error?.message || "unknown error"); + reject(new this.globals.Error(message)); + }); + } catch (e3) { + reject(e3); + } + }); + } + /** + * @param {import('../index.js').Subscription} msg + * @param {(value: unknown | undefined) => void} callback + */ + subscribe(msg, callback) { + const comparator = (eventData) => { + return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.subscriptionName === msg.subscriptionName; + }; + const cb = (eventData) => { + return callback(eventData.params); + }; + return this._subscribe(comparator, {}, cb); + } + /** + * @typedef {import('../index.js').MessageResponse | import('../index.js').SubscriptionEvent} Incoming + */ + /** + * @param {(eventData: any) => boolean} comparator + * @param {{signal?: AbortSignal}} options + * @param {(value: Incoming, unsubscribe: (()=>void)) => void} callback + * @internal + */ + _subscribe(comparator, options, callback) { + if (options?.signal?.aborted) { + throw new DOMException("Aborted", "AbortError"); + } + let teardown; + const idHandler = (event) => { + if (this.messagingContext.env === "production") { + if (event.origin !== null && event.origin !== void 0) { + console.warn("ignoring because evt.origin is not `null` or `undefined`"); + return; + } + } + if (!event.data) { + console.warn("data absent from message"); + return; + } + if (comparator(event.data)) { + if (!teardown) + throw new Error("unreachable"); + callback(event.data, teardown); + } + }; + const abortHandler = () => { + teardown?.(); + throw new DOMException("Aborted", "AbortError"); + }; + this.config.methods.addEventListener("message", idHandler); + options?.signal?.addEventListener("abort", abortHandler); + teardown = () => { + this.config.methods.removeEventListener("message", idHandler); + options?.signal?.removeEventListener("abort", abortHandler); + }; + return () => { + teardown?.(); + }; + } + }; + var WindowsMessagingConfig = class { + /** + * @param {object} params + * @param {WindowsInteropMethods} params.methods + * @internal + */ + constructor(params) { + this.methods = params.methods; + this.platform = "windows"; + } + }; + var WindowsNotification = class { + /** + * @param {object} params + * @param {string} params.Feature + * @param {string} params.SubFeatureName + * @param {string} params.Name + * @param {Record} [params.Data] + * @internal + */ + constructor(params) { + this.Feature = params.Feature; + this.SubFeatureName = params.SubFeatureName; + this.Name = params.Name; + this.Data = params.Data; + } + /** + * Helper to convert a {@link NotificationMessage} to a format that Windows can support + * @param {NotificationMessage} notification + * @returns {WindowsNotification} + */ + static fromNotification(notification, data) { + const output = { + Data: data, + Feature: notification.context, + SubFeatureName: notification.featureName, + Name: notification.method + }; + return output; + } + }; + var WindowsRequestMessage = class { + /** + * @param {object} params + * @param {string} params.Feature + * @param {string} params.SubFeatureName + * @param {string} params.Name + * @param {Record} [params.Data] + * @param {string} [params.Id] + * @internal + */ + constructor(params) { + this.Feature = params.Feature; + this.SubFeatureName = params.SubFeatureName; + this.Name = params.Name; + this.Data = params.Data; + this.Id = params.Id; + } + /** + * Helper to convert a {@link RequestMessage} to a format that Windows can support + * @param {RequestMessage} msg + * @param {Record} data + * @returns {WindowsRequestMessage} + */ + static fromRequest(msg, data) { + const output = { + Data: data, + Feature: msg.context, + SubFeatureName: msg.featureName, + Name: msg.method, + Id: msg.id + }; + return output; + } + }; + + // ../messaging/schema.js + var RequestMessage = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.method + * @param {string} params.id + * @param {Record} [params.params] + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.method = params.method; + this.id = params.id; + this.params = params.params; + } + }; + var NotificationMessage = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.method + * @param {Record} [params.params] + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.method = params.method; + this.params = params.params; + } + }; + var Subscription = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.subscriptionName + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.subscriptionName = params.subscriptionName; + } + }; + function isResponseFor(request, data) { + if ("result" in data) { + return data.featureName === request.featureName && data.context === request.context && data.id === request.id; + } + if ("error" in data) { + if ("message" in data.error) { + return true; + } + } + return false; + } + function isSubscriptionEventFor(sub, data) { + if ("subscriptionName" in data) { + return data.featureName === sub.featureName && data.context === sub.context && data.subscriptionName === sub.subscriptionName; + } + return false; + } + + // ../messaging/lib/webkit.js + var WebkitMessagingTransport = class { + /** + * @param {WebkitMessagingConfig} config + * @param {import('../index.js').MessagingContext} messagingContext + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + this.globals = captureGlobals(); + if (!this.config.hasModernWebkitAPI) { + this.captureWebkitHandlers(this.config.webkitMessageHandlerNames); + } + } + /** + * Sends message to the webkit layer (fire and forget) + * @param {String} handler + * @param {*} data + * @internal + */ + wkSend(handler, data = {}) { + if (!(handler in this.globals.window.webkit.messageHandlers)) { + throw new MissingHandler(`Missing webkit handler: '${handler}'`, handler); + } + if (!this.config.hasModernWebkitAPI) { + const outgoing = { + ...data, + messageHandling: { + ...data.messageHandling, + secret: this.config.secret + } + }; + if (!(handler in this.globals.capturedWebkitHandlers)) { + throw new MissingHandler(`cannot continue, method ${handler} not captured on macos < 11`, handler); + } else { + return this.globals.capturedWebkitHandlers[handler](outgoing); + } + } + return this.globals.window.webkit.messageHandlers[handler].postMessage?.(data); + } + /** + * Sends message to the webkit layer and waits for the specified response + * @param {String} handler + * @param {import('../index.js').RequestMessage} data + * @returns {Promise<*>} + * @internal + */ + async wkSendAndWait(handler, data) { + if (this.config.hasModernWebkitAPI) { + const response = await this.wkSend(handler, data); + return this.globals.JSONparse(response || "{}"); + } + try { + const randMethodName = this.createRandMethodName(); + const key = await this.createRandKey(); + const iv = this.createRandIv(); + const { + ciphertext, + tag + } = await new this.globals.Promise((resolve) => { + this.generateRandomMethod(randMethodName, resolve); + data.messageHandling = new SecureMessagingParams({ + methodName: randMethodName, + secret: this.config.secret, + key: this.globals.Arrayfrom(key), + iv: this.globals.Arrayfrom(iv) + }); + this.wkSend(handler, data); + }); + const cipher = new this.globals.Uint8Array([...ciphertext, ...tag]); + const decrypted = await this.decrypt(cipher, key, iv); + return this.globals.JSONparse(decrypted || "{}"); + } catch (e3) { + if (e3 instanceof MissingHandler) { + throw e3; + } else { + console.error("decryption failed", e3); + console.error(e3); + return { error: e3 }; + } + } + } + /** + * @param {import('../index.js').NotificationMessage} msg + */ + notify(msg) { + this.wkSend(msg.context, msg); + } + /** + * @param {import('../index.js').RequestMessage} msg + */ + async request(msg) { + const data = await this.wkSendAndWait(msg.context, msg); + if (isResponseFor(msg, data)) { + if (data.result) { + return data.result || {}; + } + if (data.error) { + throw new Error(data.error.message); + } + } + throw new Error("an unknown error occurred"); + } + /** + * Generate a random method name and adds it to the global scope + * The native layer will use this method to send the response + * @param {string | number} randomMethodName + * @param {Function} callback + * @internal + */ + generateRandomMethod(randomMethodName, callback) { + this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, { + enumerable: false, + // configurable, To allow for deletion later + configurable: true, + writable: false, + /** + * @param {any[]} args + */ + value: (...args) => { + callback(...args); + delete this.globals.window[randomMethodName]; + } + }); + } + /** + * @internal + * @return {string} + */ + randomString() { + return "" + this.globals.getRandomValues(new this.globals.Uint32Array(1))[0]; + } + /** + * @internal + * @return {string} + */ + createRandMethodName() { + return "_" + this.randomString(); + } + /** + * @type {{name: string, length: number}} + * @internal + */ + algoObj = { + name: "AES-GCM", + length: 256 + }; + /** + * @returns {Promise} + * @internal + */ + async createRandKey() { + const key = await this.globals.generateKey(this.algoObj, true, ["encrypt", "decrypt"]); + const exportedKey = await this.globals.exportKey("raw", key); + return new this.globals.Uint8Array(exportedKey); + } + /** + * @returns {Uint8Array} + * @internal + */ + createRandIv() { + return this.globals.getRandomValues(new this.globals.Uint8Array(12)); + } + /** + * @param {BufferSource} ciphertext + * @param {BufferSource} key + * @param {Uint8Array} iv + * @returns {Promise} + * @internal + */ + async decrypt(ciphertext, key, iv) { + const cryptoKey = await this.globals.importKey("raw", key, "AES-GCM", false, ["decrypt"]); + const algo = { + name: "AES-GCM", + iv + }; + const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); + const dec = new this.globals.TextDecoder(); + return dec.decode(decrypted); + } + /** + * When required (such as on macos 10.x), capture the `postMessage` method on + * each webkit messageHandler + * + * @param {string[]} handlerNames + */ + captureWebkitHandlers(handlerNames) { + const handlers = window.webkit.messageHandlers; + if (!handlers) + throw new MissingHandler("window.webkit.messageHandlers was absent", "all"); + for (const webkitMessageHandlerName of handlerNames) { + if (typeof handlers[webkitMessageHandlerName]?.postMessage === "function") { + const original = handlers[webkitMessageHandlerName]; + const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original); + this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound; + delete handlers[webkitMessageHandlerName].postMessage; + } + } + } + /** + * @param {import('../index.js').Subscription} msg + * @param {(value: unknown) => void} callback + */ + subscribe(msg, callback) { + if (msg.subscriptionName in this.globals.window) { + throw new this.globals.Error(`A subscription with the name ${msg.subscriptionName} already exists`); + } + this.globals.ObjectDefineProperty(this.globals.window, msg.subscriptionName, { + enumerable: false, + configurable: true, + writable: false, + value: (data) => { + if (data && isSubscriptionEventFor(msg, data)) { + callback(data.params); + } else { + console.warn("Received a message that did not match the subscription", data); + } + } + }); + return () => { + this.globals.ReflectDeleteProperty(this.globals.window, msg.subscriptionName); + }; + } + }; + var WebkitMessagingConfig = class { + /** + * @param {object} params + * @param {boolean} params.hasModernWebkitAPI + * @param {string[]} params.webkitMessageHandlerNames + * @param {string} params.secret + * @internal + */ + constructor(params) { + this.hasModernWebkitAPI = params.hasModernWebkitAPI; + this.webkitMessageHandlerNames = params.webkitMessageHandlerNames; + this.secret = params.secret; + } + }; + var SecureMessagingParams = class { + /** + * @param {object} params + * @param {string} params.methodName + * @param {string} params.secret + * @param {number[]} params.key + * @param {number[]} params.iv + */ + constructor(params) { + this.methodName = params.methodName; + this.secret = params.secret; + this.key = params.key; + this.iv = params.iv; + } + }; + function captureGlobals() { + const globals = { + window, + getRandomValues: window.crypto.getRandomValues.bind(window.crypto), + TextEncoder, + TextDecoder, + Uint8Array, + Uint16Array, + Uint32Array, + JSONstringify: window.JSON.stringify, + JSONparse: window.JSON.parse, + Arrayfrom: window.Array.from, + Promise: window.Promise, + Error: window.Error, + ReflectDeleteProperty: window.Reflect.deleteProperty.bind(window.Reflect), + ObjectDefineProperty: window.Object.defineProperty, + addEventListener: window.addEventListener.bind(window), + /** @type {Record} */ + capturedWebkitHandlers: {} + }; + if (isSecureContext) { + globals.generateKey = window.crypto.subtle.generateKey.bind(window.crypto.subtle); + globals.exportKey = window.crypto.subtle.exportKey.bind(window.crypto.subtle); + globals.importKey = window.crypto.subtle.importKey.bind(window.crypto.subtle); + globals.encrypt = window.crypto.subtle.encrypt.bind(window.crypto.subtle); + globals.decrypt = window.crypto.subtle.decrypt.bind(window.crypto.subtle); + } + return globals; + } + + // ../messaging/lib/android.js + var AndroidMessagingTransport = class { + /** + * @param {AndroidMessagingConfig} config + * @param {MessagingContext} messagingContext + * @internal + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + } + /** + * @param {NotificationMessage} msg + */ + notify(msg) { + try { + this.config.sendMessageThrows?.(JSON.stringify(msg)); + } catch (e3) { + console.error(".notify failed", e3); + } + } + /** + * @param {RequestMessage} msg + * @return {Promise} + */ + request(msg) { + return new Promise((resolve, reject) => { + const unsub = this.config.subscribe(msg.id, handler); + try { + this.config.sendMessageThrows?.(JSON.stringify(msg)); + } catch (e3) { + unsub(); + reject(new Error("request failed to send: " + e3.message || "unknown error")); + } + function handler(data) { + if (isResponseFor(msg, data)) { + if (data.result) { + resolve(data.result || {}); + return unsub(); + } + if (data.error) { + reject(new Error(data.error.message)); + return unsub(); + } + unsub(); + throw new Error("unreachable: must have `result` or `error` key by this point"); + } + } + }); + } + /** + * @param {Subscription} msg + * @param {(value: unknown | undefined) => void} callback + */ + subscribe(msg, callback) { + const unsub = this.config.subscribe(msg.subscriptionName, (data) => { + if (isSubscriptionEventFor(msg, data)) { + callback(data.params || {}); + } + }); + return () => { + unsub(); + }; + } + }; + var AndroidMessagingConfig = class { + /** @type {(json: string, secret: string) => void} */ + _capturedHandler; + /** + * @param {object} params + * @param {Record} params.target + * @param {boolean} params.debug + * @param {string} params.messageSecret - a secret to ensure that messages are only + * processed by the correct handler + * @param {string} params.javascriptInterface - the name of the javascript interface + * registered on the native side + * @param {string} params.messageCallback - the name of the callback that the native + * side will use to send messages back to the javascript side + */ + constructor(params) { + this.target = params.target; + this.debug = params.debug; + this.javascriptInterface = params.javascriptInterface; + this.messageSecret = params.messageSecret; + this.messageCallback = params.messageCallback; + this.listeners = new globalThis.Map(); + this._captureGlobalHandler(); + this._assignHandlerMethod(); + } + /** + * The transport can call this to transmit a JSON payload along with a secret + * to the native Android handler. + * + * Note: This can throw - it's up to the transport to handle the error. + * + * @type {(json: string) => void} + * @throws + * @internal + */ + sendMessageThrows(json) { + this._capturedHandler(json, this.messageSecret); + } + /** + * A subscription on Android is just a named listener. All messages from + * android -> are delivered through a single function, and this mapping is used + * to route the messages to the correct listener. + * + * Note: Use this to implement request->response by unsubscribing after the first + * response. + * + * @param {string} id + * @param {(msg: MessageResponse | SubscriptionEvent) => void} callback + * @returns {() => void} + * @internal + */ + subscribe(id, callback) { + this.listeners.set(id, callback); + return () => { + this.listeners.delete(id); + }; + } + /** + * Accept incoming messages and try to deliver it to a registered listener. + * + * This code is defensive to prevent any single handler from affecting another if + * it throws (producer interference). + * + * @param {MessageResponse | SubscriptionEvent} payload + * @internal + */ + _dispatch(payload) { + if (!payload) + return this._log("no response"); + if ("id" in payload) { + if (this.listeners.has(payload.id)) { + this._tryCatch(() => this.listeners.get(payload.id)?.(payload)); + } else { + this._log("no listeners for ", payload); + } + } + if ("subscriptionName" in payload) { + if (this.listeners.has(payload.subscriptionName)) { + this._tryCatch(() => this.listeners.get(payload.subscriptionName)?.(payload)); + } else { + this._log("no subscription listeners for ", payload); + } + } + } + /** + * + * @param {(...args: any[]) => any} fn + * @param {string} [context] + */ + _tryCatch(fn, context = "none") { + try { + return fn(); + } catch (e3) { + if (this.debug) { + console.error("AndroidMessagingConfig error:", context); + console.error(e3); + } + } + } + /** + * @param {...any} args + */ + _log(...args) { + if (this.debug) { + console.log("AndroidMessagingConfig", ...args); + } + } + /** + * Capture the global handler and remove it from the global object. + */ + _captureGlobalHandler() { + const { target, javascriptInterface } = this; + if (Object.prototype.hasOwnProperty.call(target, javascriptInterface)) { + this._capturedHandler = target[javascriptInterface].process.bind(target[javascriptInterface]); + delete target[javascriptInterface]; + } else { + this._capturedHandler = () => { + this._log("Android messaging interface not available", javascriptInterface); + }; + } + } + /** + * Assign the incoming handler method to the global object. + * This is the method that Android will call to deliver messages. + */ + _assignHandlerMethod() { + const responseHandler = (providedSecret, response) => { + if (providedSecret === this.messageSecret) { + this._dispatch(response); + } + }; + Object.defineProperty(this.target, this.messageCallback, { + value: responseHandler + }); + } + }; + + // ../messaging/lib/typed-messages.js + function createTypedMessages(base, messaging2) { + const asAny = ( + /** @type {any} */ + messaging2 + ); + return ( + /** @type {BaseClass} */ + asAny + ); + } + + // ../messaging/index.js + var MessagingContext2 = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {"production" | "development"} params.env + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.env = params.env; + } + }; + var Messaging = class { + /** + * @param {MessagingContext} messagingContext + * @param {MessagingConfig} config + */ + constructor(messagingContext, config) { + this.messagingContext = messagingContext; + this.transport = getTransport(config, this.messagingContext); + } + /** + * Send a 'fire-and-forget' message. + * @throws {MissingHandler} + * + * @example + * + * ```ts + * const messaging = new Messaging(config) + * messaging.notify("foo", {bar: "baz"}) + * ``` + * @param {string} name + * @param {Record} [data] + */ + notify(name, data = {}) { + const message = new NotificationMessage({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + method: name, + params: data + }); + this.transport.notify(message); + } + /** + * Send a request, and wait for a response + * @throws {MissingHandler} + * + * @example + * ``` + * const messaging = new Messaging(config) + * const response = await messaging.request("foo", {bar: "baz"}) + * ``` + * + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ + request(name, data = {}) { + const id = globalThis?.crypto?.randomUUID?.() || name + ".response"; + const message = new RequestMessage({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + method: name, + params: data, + id + }); + return this.transport.request(message); + } + /** + * @param {string} name + * @param {(value: unknown) => void} callback + * @return {() => void} + */ + subscribe(name, callback) { + const msg = new Subscription({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + subscriptionName: name + }); + return this.transport.subscribe(msg, callback); + } + }; + var TestTransportConfig = class { + /** + * @param {MessagingTransport} impl + */ + constructor(impl) { + this.impl = impl; + } + }; + var TestTransport = class { + /** + * @param {TestTransportConfig} config + * @param {MessagingContext} messagingContext + */ + constructor(config, messagingContext) { + this.config = config; + this.messagingContext = messagingContext; + } + notify(msg) { + return this.config.impl.notify(msg); + } + request(msg) { + return this.config.impl.request(msg); + } + subscribe(msg, callback) { + return this.config.impl.subscribe(msg, callback); + } + }; + function getTransport(config, messagingContext) { + if (config instanceof WebkitMessagingConfig) { + return new WebkitMessagingTransport(config, messagingContext); + } + if (config instanceof WindowsMessagingConfig) { + return new WindowsMessagingTransport(config, messagingContext); + } + if (config instanceof AndroidMessagingConfig) { + return new AndroidMessagingTransport(config, messagingContext); + } + if (config instanceof TestTransportConfig) { + return new TestTransport(config, messagingContext); + } + throw new Error("unreachable"); + } + var MissingHandler = class extends Error { + /** + * @param {string} message + * @param {string} handlerName + */ + constructor(message, handlerName) { + super(message); + this.handlerName = handlerName; + } + }; + + // shared/create-special-page-messaging.js + function createSpecialPageMessaging(opts) { + const messageContext = new MessagingContext2({ + context: "specialPages", + featureName: opts.pageName, + env: opts.env + }); + try { + if (opts.injectName === "windows") { + const opts2 = new WindowsMessagingConfig({ + methods: { + // @ts-expect-error - not in @types/chrome + postMessage: window.chrome.webview.postMessage, + // @ts-expect-error - not in @types/chrome + addEventListener: window.chrome.webview.addEventListener, + // @ts-expect-error - not in @types/chrome + removeEventListener: window.chrome.webview.removeEventListener + } + }); + return new Messaging(messageContext, opts2); + } else if (opts.injectName === "apple") { + const opts2 = new WebkitMessagingConfig({ + hasModernWebkitAPI: true, + secret: "", + webkitMessageHandlerNames: ["specialPages"] + }); + return new Messaging(messageContext, opts2); + } else if (opts.injectName === "android") { + const opts2 = new AndroidMessagingConfig({ + messageSecret: "duckduckgo-android-messaging-secret", + messageCallback: "messageCallback", + javascriptInterface: messageContext.context, + target: globalThis, + debug: true + }); + return new Messaging(messageContext, opts2); + } + } catch (e3) { + console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); + } + const fallback = opts.mockTransport?.() || new TestTransportConfig({ + /** + * @param {import('@duckduckgo/messaging').NotificationMessage} msg + */ + notify(msg) { + console.log(msg); + }, + /** + * @param {import('@duckduckgo/messaging').RequestMessage} msg + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + request: (msg) => { + console.log(msg); + if (msg.method === "initialSetup") { + return Promise.resolve({ + locale: "en", + env: opts.env + }); + } + return Promise.resolve(null); + }, + /** + * @param {import('@duckduckgo/messaging').SubscriptionEvent} msg + */ + subscribe(msg) { + console.log(msg); + return () => { + console.log("teardown"); + }; + } + }); + return new Messaging(messageContext, fallback); + } + + // shared/environment.js + var Environment = class _Environment { + /** + * @param {object} params + * @param {'app' | 'components'} [params.display] - whether to show the application or component list + * @param {'production' | 'development'} [params.env] - application environment + * @param {URLSearchParams} [params.urlParams] - URL params passed into the page + * @param {ImportMeta['injectName']} [params.injectName] - application platform + * @param {boolean} [params.willThrow] - whether the application will simulate an error + * @param {boolean} [params.debugState] - whether to show debugging UI + * @param {string} [params.locale] - for applications strings + * @param {number} [params.textLength] - what ratio of text should be used. Set a number higher than 1 to have longer strings for testing + */ + constructor({ + env = "production", + urlParams = new URLSearchParams(location.search), + injectName = "windows", + willThrow = urlParams.get("willThrow") === "true", + debugState = urlParams.has("debugState"), + display = "app", + locale = "en", + textLength = 1 + } = {}) { + this.display = display; + this.urlParams = urlParams; + this.injectName = injectName; + this.willThrow = willThrow; + this.debugState = debugState; + this.env = env; + this.locale = locale; + this.textLength = textLength; + } + /** + * @param {string|null|undefined} injectName + * @returns {Environment} + */ + withInjectName(injectName) { + if (!injectName) + return this; + if (!isInjectName(injectName)) + return this; + return new _Environment({ + ...this, + injectName + }); + } + /** + * @param {string|null|undefined} env + * @returns {Environment} + */ + withEnv(env) { + if (!env) + return this; + if (env !== "production" && env !== "development") + return this; + return new _Environment({ + ...this, + env + }); + } + /** + * @param {string|null|undefined} display + * @returns {Environment} + */ + withDisplay(display) { + if (!display) + return this; + if (display !== "app" && display !== "components") + return this; + return new _Environment({ + ...this, + display + }); + } + /** + * @param {string|null|undefined} locale + * @returns {Environment} + */ + withLocale(locale) { + if (!locale) + return this; + if (typeof locale !== "string") + return this; + if (locale.length !== 2) + return this; + return new _Environment({ + ...this, + locale + }); + } + /** + * @param {string|number|null|undefined} length + * @returns {Environment} + */ + withTextLength(length) { + if (!length) + return this; + const num = Number(length); + if (num >= 1 && num <= 2) { + return new _Environment({ + ...this, + textLength: num + }); + } + return this; + } + }; + function isInjectName(input) { + const allowed = ["windows", "apple", "integration", "android"]; + return allowed.includes(input); + } + + // pages/new-tab/src/js/mock-transport.js + function mockTransport() { + const channel = new BroadcastChannel("ntp"); + function broadcast() { + setTimeout(() => { + channel.postMessage({ + change: "ntp.widgetConfig" + }); + }, 100); + } + function read(name) { + try { + const item = localStorage.getItem(name); + if (!item) + return null; + console.log("did read from LS", item); + return JSON.parse(item); + } catch (e3) { + console.error("Failed to parse initialSetup from localStorage", e3); + return null; + } + } + function write(name, value) { + try { + localStorage.setItem(name, JSON.stringify(value)); + console.log("\u2705 did write"); + } catch (e3) { + console.error("Failed to write", e3); + } + } + return new TestTransportConfig({ + notify(msg) { + switch (msg.method) { + case "setWidgetConfig": { + if (!msg.params) + throw new Error("unreachable"); + write("ntp.widgetConfig", msg.params); + broadcast(); + return; + } + default: { + console.warn("unhandled notification", msg); + } + } + }, + subscribe(sub, cb) { + switch (sub.subscriptionName) { + case "onWidgetConfigUpdated": { + const controller = new AbortController(); + channel.addEventListener("message", () => { + const values = read("ntp.widgetConfig"); + if (values) { + cb(values); + } + }, { signal: controller.signal }); + return () => controller.abort(); + } + } + return () => { + }; + }, + // eslint-ignore-next-line require-await + request(msg) { + switch (msg.method) { + case "initialSetup": { + const widgetsFromStorage = read("ntp.widgets") || { + widgets: [ + { id: "favorites" }, + { id: "privacyStats" } + ] + }; + const widgetConfigFromStorage = read("ntp.widgetConfig") || { + widgetConfig: [ + { id: "favorites", visibility: "visible" }, + { id: "privacyStats", visibility: "visible" } + ] + }; + return Promise.resolve({ + widgets: widgetsFromStorage.widgets, + widgetConfig: widgetConfigFromStorage.widgetConfig, + platform: { name: "integration" }, + env: "development", + locale: "en" + }); + } + default: { + return Promise.reject(new Error("unhandled request")); + } + } + } + }); + } + + // pages/new-tab/src/js/index.js + var NewTabPage = class { + /** + * @param {import("@duckduckgo/messaging").Messaging} messaging + * @param {ImportMeta['injectName']} injectName + */ + constructor(messaging2, injectName) { + this.messaging = createTypedMessages(this, messaging2); + this.injectName = injectName; + } + /** + * @return {Promise} + */ + init() { + return this.messaging.request("initialSetup"); + } + /** + * @param {string} message + */ + reportInitException(message) { + this.messaging.notify("reportInitException", { message }); + } + /** + * This will be sent if the application has loaded, but a client-side error + * has occurred that cannot be recovered from + * @param {{message: string}} params + */ + reportPageException(params) { + this.messaging.notify("reportPageException", params); + } + }; + var baseEnvironment = new Environment().withInjectName("integration").withEnv("production"); + var messaging = createSpecialPageMessaging({ + injectName: "integration", + env: "production", + pageName: "newTabPage", + mockTransport: () => { + if (baseEnvironment.injectName !== "integration") + return null; + if (window.__playwright_01) + return null; + let mock = null; + $INTEGRATION: + mock = mockTransport(); + return mock; + } + }); + var newTabMessaging = new NewTabPage(messaging, "integration"); + init(newTabMessaging, baseEnvironment).catch((e3) => { + console.error(e3); + const msg = typeof e3?.message === "string" ? e3.message : "unknown init error"; + newTabMessaging.reportInitException(msg); + }); +})(); diff --git a/build/integration/pages/new-tab/js/inline.js b/build/integration/pages/new-tab/js/inline.js new file mode 100644 index 000000000..5a71d2623 --- /dev/null +++ b/build/integration/pages/new-tab/js/inline.js @@ -0,0 +1,3 @@ +"use strict"; +(() => { +})(); diff --git a/build/integration/pages/new-tab/js/mock-transport.js b/build/integration/pages/new-tab/js/mock-transport.js new file mode 100644 index 000000000..ca6b115d2 --- /dev/null +++ b/build/integration/pages/new-tab/js/mock-transport.js @@ -0,0 +1,103 @@ +import { TestTransportConfig } from '@duckduckgo/messaging' + +export function mockTransport () { + const channel = new BroadcastChannel('ntp') + + function broadcast () { + setTimeout(() => { + channel.postMessage({ + change: 'ntp.widgetConfig' + }) + }, 100) + } + + /** + * @param {string} name + * @return {Record|null} + */ + function read (name) { + try { + const item = localStorage.getItem(name) + if (!item) return null + console.log('did read from LS', item) + return JSON.parse(item) + } catch (e) { + console.error('Failed to parse initialSetup from localStorage', e) + return null + } + } + + /** + * @param {string} name + * @param {Record} value + */ + function write (name, value) { + try { + localStorage.setItem(name, JSON.stringify(value)) + console.log('✅ did write') + } catch (e) { + console.error('Failed to write', e) + } + } + + return new TestTransportConfig({ + notify (msg) { + switch (msg.method) { + case 'setWidgetConfig': { + if (!msg.params) throw new Error('unreachable') + write('ntp.widgetConfig', msg.params) + broadcast() + return + } + default: { + console.warn('unhandled notification', msg) + } + } + }, + subscribe (sub, cb) { + switch (sub.subscriptionName) { + case 'onWidgetConfigUpdated': { + const controller = new AbortController() + // console.log('sub?', sub, cb); + channel.addEventListener('message', () => { + const values = read('ntp.widgetConfig') + if (values) { + cb(values) + } + }, { signal: controller.signal }) + return () => controller.abort() + } + } + return () => {} + }, + // eslint-ignore-next-line require-await + request (msg) { + switch (msg.method) { + case 'initialSetup': { + const widgetsFromStorage = read('ntp.widgets') || { + widgets: [ + { id: 'favorites' }, + { id: 'privacyStats' } + ] + } + const widgetConfigFromStorage = read('ntp.widgetConfig') || { + widgetConfig: [ + { id: 'favorites', visibility: 'visible' }, + { id: 'privacyStats', visibility: 'visible' } + ] + } + return Promise.resolve({ + widgets: widgetsFromStorage.widgets, + widgetConfig: widgetConfigFromStorage.widgetConfig, + platform: { name: 'integration' }, + env: 'development', + locale: 'en' + }) + } + default: { + return Promise.reject(new Error('unhandled request')) + } + } + } + }) +} diff --git a/build/integration/pages/new-tab/locales/en/newtab.json b/build/integration/pages/new-tab/locales/en/newtab.json new file mode 100644 index 000000000..6d61912fc --- /dev/null +++ b/build/integration/pages/new-tab/locales/en/newtab.json @@ -0,0 +1,16 @@ +{ + "smartling": { + "string_format": "icu", + "translate_paths": [ + { + "path": "*/title", + "key": "{*}/title", + "instruction": "*/note" + } + ] + }, + "helloWorld": { + "title": "Hello world!", + "note": "here!" + } +} diff --git a/build/integration/pages/onboarding/js/index.js b/build/integration/pages/onboarding/js/index.js index d77743314..998401264 100644 --- a/build/integration/pages/onboarding/js/index.js +++ b/build/integration/pages/onboarding/js/index.js @@ -8440,7 +8440,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/build/integration/pages/release-notes/js/index.js b/build/integration/pages/release-notes/js/index.js index 29a845dc1..51ee5b01b 100644 --- a/build/integration/pages/release-notes/js/index.js +++ b/build/integration/pages/release-notes/js/index.js @@ -2051,7 +2051,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/build/integration/pages/special-error/js/index.js b/build/integration/pages/special-error/js/index.js index cb6005c4b..76e9c9512 100644 --- a/build/integration/pages/special-error/js/index.js +++ b/build/integration/pages/special-error/js/index.js @@ -1135,7 +1135,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/build/windows/pages/duckplayer/js/index.css b/build/windows/pages/duckplayer/js/index.css index de2906b8f..f0299a0ba 100644 --- a/build/windows/pages/duckplayer/js/index.css +++ b/build/windows/pages/duckplayer/js/index.css @@ -48,7 +48,7 @@ body[data-display=app] { padding: 16px; } -/* pages/duckplayer/app/components/Fallback.module.css */ +/* shared/components/Fallback/Fallback.module.css */ .Fallback_fallback { height: 100%; width: 100%; diff --git a/build/windows/pages/duckplayer/js/index.js b/build/windows/pages/duckplayer/js/index.js index 3e234ae3e..e0d2ca16c 100644 --- a/build/windows/pages/duckplayer/js/index.js +++ b/build/windows/pages/duckplayer/js/index.js @@ -1135,7 +1135,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ @@ -2258,12 +2258,12 @@ return q2(UserValuesContext).setEnabled; } - // pages/duckplayer/app/components/Fallback.module.css + // shared/components/Fallback/Fallback.module.css var Fallback_default = { fallback: "Fallback_fallback" }; - // pages/duckplayer/app/components/Fallback.jsx + // shared/components/Fallback/Fallback.jsx function Fallback({ showDetails }) { return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); } diff --git a/build/windows/pages/new-tab/index.html b/build/windows/pages/new-tab/index.html new file mode 100644 index 000000000..6d0102db4 --- /dev/null +++ b/build/windows/pages/new-tab/index.html @@ -0,0 +1,14 @@ + + + + New Tab Page + + + + + + +
+ + + diff --git a/build/windows/pages/new-tab/js/index.css b/build/windows/pages/new-tab/js/index.css new file mode 100644 index 000000000..55225c2b1 --- /dev/null +++ b/build/windows/pages/new-tab/js/index.css @@ -0,0 +1,58 @@ +/* pages/new-tab/app/styles/base.css */ +*, +*:after, +*:before { + box-sizing: border-box; +} +html[data-reduced-motion=true] * { + animation: none !important; + transition: none !important; +} +body { + font-family: system-ui; + margin: 0; + height: 100vh; + width: 100%; + overflow-x: hidden; + user-select: none; + -webkit-user-select: none; + cursor: default; + color: var(--theme-txt-color); + background: var(--theme-page-bg); +} +body > main { + width: 100%; +} +h1, +h2, +h3, +h4 { + margin: 0; +} +button { + font-family: system-ui, sans-serif; +} +ul { + margin: 0; + padding: 0; +} +li { + list-style: none; + margin: 0; + padding: 0; +} + +/* pages/new-tab/app/components/App.module.css */ +.App_layout { + padding-top: var(--sp-16); + padding-bottom: var(--sp-16); + max-width: 504px; + margin-left: auto; + margin-right: auto; +} + +/* shared/components/Fallback/Fallback.module.css */ +.Fallback_fallback { + height: 100%; + width: 100%; +} diff --git a/build/windows/pages/new-tab/js/index.js b/build/windows/pages/new-tab/js/index.js new file mode 100644 index 000000000..ba4c84271 --- /dev/null +++ b/build/windows/pages/new-tab/js/index.js @@ -0,0 +1,2078 @@ +"use strict"; +(() => { + // ../../node_modules/preact/dist/preact.module.js + var n; + var l; + var u; + var t; + var i; + var o; + var r; + var f; + var e; + var c = {}; + var s = []; + var a = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; + var h = Array.isArray; + function v(n2, l3) { + for (var u3 in l3) + n2[u3] = l3[u3]; + return n2; + } + function p(n2) { + var l3 = n2.parentNode; + l3 && l3.removeChild(n2); + } + function y(l3, u3, t3) { + var i3, o3, r3, f3 = {}; + for (r3 in u3) + "key" == r3 ? i3 = u3[r3] : "ref" == r3 ? o3 = u3[r3] : f3[r3] = u3[r3]; + if (arguments.length > 2 && (f3.children = arguments.length > 3 ? n.call(arguments, 2) : t3), "function" == typeof l3 && null != l3.defaultProps) + for (r3 in l3.defaultProps) + void 0 === f3[r3] && (f3[r3] = l3.defaultProps[r3]); + return d(l3, f3, i3, o3, null); + } + function d(n2, t3, i3, o3, r3) { + var f3 = { type: n2, props: t3, key: i3, ref: o3, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, constructor: void 0, __v: null == r3 ? ++u : r3, __i: -1, __u: 0 }; + return null == r3 && null != l.vnode && l.vnode(f3), f3; + } + function g(n2) { + return n2.children; + } + function b(n2, l3) { + this.props = n2, this.context = l3; + } + function m(n2, l3) { + if (null == l3) + return n2.__ ? m(n2.__, n2.__i + 1) : null; + for (var u3; l3 < n2.__k.length; l3++) + if (null != (u3 = n2.__k[l3]) && null != u3.__e) + return u3.__e; + return "function" == typeof n2.type ? m(n2) : null; + } + function k(n2) { + var l3, u3; + if (null != (n2 = n2.__) && null != n2.__c) { + for (n2.__e = n2.__c.base = null, l3 = 0; l3 < n2.__k.length; l3++) + if (null != (u3 = n2.__k[l3]) && null != u3.__e) { + n2.__e = n2.__c.base = u3.__e; + break; + } + return k(n2); + } + } + function w(n2) { + (!n2.__d && (n2.__d = true) && i.push(n2) && !x.__r++ || o !== l.debounceRendering) && ((o = l.debounceRendering) || r)(x); + } + function x() { + var n2, u3, t3, o3, r3, e3, c3, s3, a3; + for (i.sort(f); n2 = i.shift(); ) + n2.__d && (u3 = i.length, o3 = void 0, e3 = (r3 = (t3 = n2).__v).__e, s3 = [], a3 = [], (c3 = t3.__P) && ((o3 = v({}, r3)).__v = r3.__v + 1, l.vnode && l.vnode(o3), L(c3, o3, r3, t3.__n, void 0 !== c3.ownerSVGElement, 32 & r3.__u ? [e3] : null, s3, null == e3 ? m(r3) : e3, !!(32 & r3.__u), a3), o3.__.__k[o3.__i] = o3, M(s3, o3, a3), o3.__e != e3 && k(o3)), i.length > u3 && i.sort(f)); + x.__r = 0; + } + function C(n2, l3, u3, t3, i3, o3, r3, f3, e3, a3, h3) { + var v3, p3, y2, d3, _2, g3 = t3 && t3.__k || s, b3 = l3.length; + for (u3.__d = e3, P(u3, l3, g3), e3 = u3.__d, v3 = 0; v3 < b3; v3++) + null != (y2 = u3.__k[v3]) && "boolean" != typeof y2 && "function" != typeof y2 && (p3 = -1 === y2.__i ? c : g3[y2.__i] || c, y2.__i = v3, L(n2, y2, p3, i3, o3, r3, f3, e3, a3, h3), d3 = y2.__e, y2.ref && p3.ref != y2.ref && (p3.ref && z(p3.ref, null, y2), h3.push(y2.ref, y2.__c || d3, y2)), null == _2 && null != d3 && (_2 = d3), 65536 & y2.__u || p3.__k === y2.__k ? e3 = S(y2, e3, n2) : "function" == typeof y2.type && void 0 !== y2.__d ? e3 = y2.__d : d3 && (e3 = d3.nextSibling), y2.__d = void 0, y2.__u &= -196609); + u3.__d = e3, u3.__e = _2; + } + function P(n2, l3, u3) { + var t3, i3, o3, r3, f3, e3 = l3.length, c3 = u3.length, s3 = c3, a3 = 0; + for (n2.__k = [], t3 = 0; t3 < e3; t3++) + null != (i3 = n2.__k[t3] = null == (i3 = l3[t3]) || "boolean" == typeof i3 || "function" == typeof i3 ? null : "string" == typeof i3 || "number" == typeof i3 || "bigint" == typeof i3 || i3.constructor == String ? d(null, i3, null, null, i3) : h(i3) ? d(g, { children: i3 }, null, null, null) : void 0 === i3.constructor && i3.__b > 0 ? d(i3.type, i3.props, i3.key, i3.ref ? i3.ref : null, i3.__v) : i3) ? (i3.__ = n2, i3.__b = n2.__b + 1, f3 = H(i3, u3, r3 = t3 + a3, s3), i3.__i = f3, o3 = null, -1 !== f3 && (s3--, (o3 = u3[f3]) && (o3.__u |= 131072)), null == o3 || null === o3.__v ? (-1 == f3 && a3--, "function" != typeof i3.type && (i3.__u |= 65536)) : f3 !== r3 && (f3 === r3 + 1 ? a3++ : f3 > r3 ? s3 > e3 - r3 ? a3 += f3 - r3 : a3-- : a3 = f3 < r3 && f3 == r3 - 1 ? f3 - r3 : 0, f3 !== t3 + a3 && (i3.__u |= 65536))) : (o3 = u3[t3]) && null == o3.key && o3.__e && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3, false), u3[t3] = null, s3--); + if (s3) + for (t3 = 0; t3 < c3; t3++) + null != (o3 = u3[t3]) && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = m(o3)), N(o3, o3)); + } + function S(n2, l3, u3) { + var t3, i3; + if ("function" == typeof n2.type) { + for (t3 = n2.__k, i3 = 0; t3 && i3 < t3.length; i3++) + t3[i3] && (t3[i3].__ = n2, l3 = S(t3[i3], l3, u3)); + return l3; + } + return n2.__e != l3 && (u3.insertBefore(n2.__e, l3 || null), l3 = n2.__e), l3 && l3.nextSibling; + } + function H(n2, l3, u3, t3) { + var i3 = n2.key, o3 = n2.type, r3 = u3 - 1, f3 = u3 + 1, e3 = l3[u3]; + if (null === e3 || e3 && i3 == e3.key && o3 === e3.type) + return u3; + if (t3 > (null != e3 && 0 == (131072 & e3.__u) ? 1 : 0)) + for (; r3 >= 0 || f3 < l3.length; ) { + if (r3 >= 0) { + if ((e3 = l3[r3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type) + return r3; + r3--; + } + if (f3 < l3.length) { + if ((e3 = l3[f3]) && 0 == (131072 & e3.__u) && i3 == e3.key && o3 === e3.type) + return f3; + f3++; + } + } + return -1; + } + function I(n2, l3, u3) { + "-" === l3[0] ? n2.setProperty(l3, null == u3 ? "" : u3) : n2[l3] = null == u3 ? "" : "number" != typeof u3 || a.test(l3) ? u3 : u3 + "px"; + } + function T(n2, l3, u3, t3, i3) { + var o3; + n: + if ("style" === l3) + if ("string" == typeof u3) + n2.style.cssText = u3; + else { + if ("string" == typeof t3 && (n2.style.cssText = t3 = ""), t3) + for (l3 in t3) + u3 && l3 in u3 || I(n2.style, l3, ""); + if (u3) + for (l3 in u3) + t3 && u3[l3] === t3[l3] || I(n2.style, l3, u3[l3]); + } + else if ("o" === l3[0] && "n" === l3[1]) + o3 = l3 !== (l3 = l3.replace(/(PointerCapture)$|Capture$/, "$1")), l3 = l3.toLowerCase() in n2 ? l3.toLowerCase().slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + o3] = u3, u3 ? t3 ? u3.u = t3.u : (u3.u = Date.now(), n2.addEventListener(l3, o3 ? D : A, o3)) : n2.removeEventListener(l3, o3 ? D : A, o3); + else { + if (i3) + l3 = l3.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s"); + else if ("width" !== l3 && "height" !== l3 && "href" !== l3 && "list" !== l3 && "form" !== l3 && "tabIndex" !== l3 && "download" !== l3 && "rowSpan" !== l3 && "colSpan" !== l3 && "role" !== l3 && l3 in n2) + try { + n2[l3] = null == u3 ? "" : u3; + break n; + } catch (n3) { + } + "function" == typeof u3 || (null == u3 || false === u3 && "-" !== l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, u3)); + } + } + function A(n2) { + var u3 = this.l[n2.type + false]; + if (n2.t) { + if (n2.t <= u3.u) + return; + } else + n2.t = Date.now(); + return u3(l.event ? l.event(n2) : n2); + } + function D(n2) { + return this.l[n2.type + true](l.event ? l.event(n2) : n2); + } + function L(n2, u3, t3, i3, o3, r3, f3, e3, c3, s3) { + var a3, p3, y2, d3, _2, m3, k3, w3, x2, P2, S2, $, H2, I2, T3, A2 = u3.type; + if (void 0 !== u3.constructor) + return null; + 128 & t3.__u && (c3 = !!(32 & t3.__u), r3 = [e3 = u3.__e = t3.__e]), (a3 = l.__b) && a3(u3); + n: + if ("function" == typeof A2) + try { + if (w3 = u3.props, x2 = (a3 = A2.contextType) && i3[a3.__c], P2 = a3 ? x2 ? x2.props.value : a3.__ : i3, t3.__c ? k3 = (p3 = u3.__c = t3.__c).__ = p3.__E : ("prototype" in A2 && A2.prototype.render ? u3.__c = p3 = new A2(w3, P2) : (u3.__c = p3 = new b(w3, P2), p3.constructor = A2, p3.render = O), x2 && x2.sub(p3), p3.props = w3, p3.state || (p3.state = {}), p3.context = P2, p3.__n = i3, y2 = p3.__d = true, p3.__h = [], p3._sb = []), null == p3.__s && (p3.__s = p3.state), null != A2.getDerivedStateFromProps && (p3.__s == p3.state && (p3.__s = v({}, p3.__s)), v(p3.__s, A2.getDerivedStateFromProps(w3, p3.__s))), d3 = p3.props, _2 = p3.state, p3.__v = u3, y2) + null == A2.getDerivedStateFromProps && null != p3.componentWillMount && p3.componentWillMount(), null != p3.componentDidMount && p3.__h.push(p3.componentDidMount); + else { + if (null == A2.getDerivedStateFromProps && w3 !== d3 && null != p3.componentWillReceiveProps && p3.componentWillReceiveProps(w3, P2), !p3.__e && (null != p3.shouldComponentUpdate && false === p3.shouldComponentUpdate(w3, p3.__s, P2) || u3.__v === t3.__v)) { + for (u3.__v !== t3.__v && (p3.props = w3, p3.state = p3.__s, p3.__d = false), u3.__e = t3.__e, u3.__k = t3.__k, u3.__k.forEach(function(n3) { + n3 && (n3.__ = u3); + }), S2 = 0; S2 < p3._sb.length; S2++) + p3.__h.push(p3._sb[S2]); + p3._sb = [], p3.__h.length && f3.push(p3); + break n; + } + null != p3.componentWillUpdate && p3.componentWillUpdate(w3, p3.__s, P2), null != p3.componentDidUpdate && p3.__h.push(function() { + p3.componentDidUpdate(d3, _2, m3); + }); + } + if (p3.context = P2, p3.props = w3, p3.__P = n2, p3.__e = false, $ = l.__r, H2 = 0, "prototype" in A2 && A2.prototype.render) { + for (p3.state = p3.__s, p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), I2 = 0; I2 < p3._sb.length; I2++) + p3.__h.push(p3._sb[I2]); + p3._sb = []; + } else + do { + p3.__d = false, $ && $(u3), a3 = p3.render(p3.props, p3.state, p3.context), p3.state = p3.__s; + } while (p3.__d && ++H2 < 25); + p3.state = p3.__s, null != p3.getChildContext && (i3 = v(v({}, i3), p3.getChildContext())), y2 || null == p3.getSnapshotBeforeUpdate || (m3 = p3.getSnapshotBeforeUpdate(d3, _2)), C(n2, h(T3 = null != a3 && a3.type === g && null == a3.key ? a3.props.children : a3) ? T3 : [T3], u3, t3, i3, o3, r3, f3, e3, c3, s3), p3.base = u3.__e, u3.__u &= -161, p3.__h.length && f3.push(p3), k3 && (p3.__E = p3.__ = null); + } catch (n3) { + u3.__v = null, c3 || null != r3 ? (u3.__e = e3, u3.__u |= c3 ? 160 : 32, r3[r3.indexOf(e3)] = null) : (u3.__e = t3.__e, u3.__k = t3.__k), l.__e(n3, u3, t3); + } + else + null == r3 && u3.__v === t3.__v ? (u3.__k = t3.__k, u3.__e = t3.__e) : u3.__e = j(t3.__e, u3, t3, i3, o3, r3, f3, c3, s3); + (a3 = l.diffed) && a3(u3); + } + function M(n2, u3, t3) { + u3.__d = void 0; + for (var i3 = 0; i3 < t3.length; i3++) + z(t3[i3], t3[++i3], t3[++i3]); + l.__c && l.__c(u3, n2), n2.some(function(u4) { + try { + n2 = u4.__h, u4.__h = [], n2.some(function(n3) { + n3.call(u4); + }); + } catch (n3) { + l.__e(n3, u4.__v); + } + }); + } + function j(l3, u3, t3, i3, o3, r3, f3, e3, s3) { + var a3, v3, y2, d3, _2, g3, b3, k3 = t3.props, w3 = u3.props, x2 = u3.type; + if ("svg" === x2 && (o3 = true), null != r3) { + for (a3 = 0; a3 < r3.length; a3++) + if ((_2 = r3[a3]) && "setAttribute" in _2 == !!x2 && (x2 ? _2.localName === x2 : 3 === _2.nodeType)) { + l3 = _2, r3[a3] = null; + break; + } + } + if (null == l3) { + if (null === x2) + return document.createTextNode(w3); + l3 = o3 ? document.createElementNS("http://www.w3.org/2000/svg", x2) : document.createElement(x2, w3.is && w3), r3 = null, e3 = false; + } + if (null === x2) + k3 === w3 || e3 && l3.data === w3 || (l3.data = w3); + else { + if (r3 = r3 && n.call(l3.childNodes), k3 = t3.props || c, !e3 && null != r3) + for (k3 = {}, a3 = 0; a3 < l3.attributes.length; a3++) + k3[(_2 = l3.attributes[a3]).name] = _2.value; + for (a3 in k3) + _2 = k3[a3], "children" == a3 || ("dangerouslySetInnerHTML" == a3 ? y2 = _2 : "key" === a3 || a3 in w3 || T(l3, a3, null, _2, o3)); + for (a3 in w3) + _2 = w3[a3], "children" == a3 ? d3 = _2 : "dangerouslySetInnerHTML" == a3 ? v3 = _2 : "value" == a3 ? g3 = _2 : "checked" == a3 ? b3 = _2 : "key" === a3 || e3 && "function" != typeof _2 || k3[a3] === _2 || T(l3, a3, _2, k3[a3], o3); + if (v3) + e3 || y2 && (v3.__html === y2.__html || v3.__html === l3.innerHTML) || (l3.innerHTML = v3.__html), u3.__k = []; + else if (y2 && (l3.innerHTML = ""), C(l3, h(d3) ? d3 : [d3], u3, t3, i3, o3 && "foreignObject" !== x2, r3, f3, r3 ? r3[0] : t3.__k && m(t3, 0), e3, s3), null != r3) + for (a3 = r3.length; a3--; ) + null != r3[a3] && p(r3[a3]); + e3 || (a3 = "value", void 0 !== g3 && (g3 !== l3[a3] || "progress" === x2 && !g3 || "option" === x2 && g3 !== k3[a3]) && T(l3, a3, g3, k3[a3], false), a3 = "checked", void 0 !== b3 && b3 !== l3[a3] && T(l3, a3, b3, k3[a3], false)); + } + return l3; + } + function z(n2, u3, t3) { + try { + "function" == typeof n2 ? n2(u3) : n2.current = u3; + } catch (n3) { + l.__e(n3, t3); + } + } + function N(n2, u3, t3) { + var i3, o3; + if (l.unmount && l.unmount(n2), (i3 = n2.ref) && (i3.current && i3.current !== n2.__e || z(i3, null, u3)), null != (i3 = n2.__c)) { + if (i3.componentWillUnmount) + try { + i3.componentWillUnmount(); + } catch (n3) { + l.__e(n3, u3); + } + i3.base = i3.__P = null, n2.__c = void 0; + } + if (i3 = n2.__k) + for (o3 = 0; o3 < i3.length; o3++) + i3[o3] && N(i3[o3], u3, t3 || "function" != typeof n2.type); + t3 || null == n2.__e || p(n2.__e), n2.__ = n2.__e = n2.__d = void 0; + } + function O(n2, l3, u3) { + return this.constructor(n2, u3); + } + function q(u3, t3, i3) { + var o3, r3, f3, e3; + l.__ && l.__(u3, t3), r3 = (o3 = "function" == typeof i3) ? null : i3 && i3.__k || t3.__k, f3 = [], e3 = [], L(t3, u3 = (!o3 && i3 || t3).__k = y(g, null, [u3]), r3 || c, c, void 0 !== t3.ownerSVGElement, !o3 && i3 ? [i3] : r3 ? null : t3.firstChild ? n.call(t3.childNodes) : null, f3, !o3 && i3 ? i3 : r3 ? r3.__e : t3.firstChild, o3, e3), M(f3, u3, e3); + } + function F(n2, l3) { + var u3 = { __c: l3 = "__cC" + e++, __: n2, Consumer: function(n3, l4) { + return n3.children(l4); + }, Provider: function(n3) { + var u4, t3; + return this.getChildContext || (u4 = [], (t3 = {})[l3] = this, this.getChildContext = function() { + return t3; + }, this.shouldComponentUpdate = function(n4) { + this.props.value !== n4.value && u4.some(function(n5) { + n5.__e = true, w(n5); + }); + }, this.sub = function(n4) { + u4.push(n4); + var l4 = n4.componentWillUnmount; + n4.componentWillUnmount = function() { + u4.splice(u4.indexOf(n4), 1), l4 && l4.call(n4); + }; + }), n3.children; + } }; + return u3.Provider.__ = u3.Consumer.contextType = u3; + } + n = s.slice, l = { __e: function(n2, l3, u3, t3) { + for (var i3, o3, r3; l3 = l3.__; ) + if ((i3 = l3.__c) && !i3.__) + try { + if ((o3 = i3.constructor) && null != o3.getDerivedStateFromError && (i3.setState(o3.getDerivedStateFromError(n2)), r3 = i3.__d), null != i3.componentDidCatch && (i3.componentDidCatch(n2, t3 || {}), r3 = i3.__d), r3) + return i3.__E = i3; + } catch (l4) { + n2 = l4; + } + throw n2; + } }, u = 0, t = function(n2) { + return null != n2 && null == n2.constructor; + }, b.prototype.setState = function(n2, l3) { + var u3; + u3 = null != this.__s && this.__s !== this.state ? this.__s : this.__s = v({}, this.state), "function" == typeof n2 && (n2 = n2(v({}, u3), this.props)), n2 && v(u3, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), w(this)); + }, b.prototype.forceUpdate = function(n2) { + this.__v && (this.__e = true, n2 && this.__h.push(n2), w(this)); + }, b.prototype.render = g, i = [], r = "function" == typeof Promise ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout, f = function(n2, l3) { + return n2.__v.__b - l3.__v.__b; + }, x.__r = 0, e = 0; + + // ../../node_modules/preact/devtools/dist/devtools.module.js + "undefined" != typeof window && window.__PREACT_DEVTOOLS__ && window.__PREACT_DEVTOOLS__.attachPreact("10.19.3", l, { Fragment: g, Component: b }); + + // pages/new-tab/app/components/App.module.css + var App_default = { + layout: "App_layout" + }; + + // pages/new-tab/app/components/App.js + function App({ children }) { + return /* @__PURE__ */ y("div", { className: App_default.layout }, children); + } + + // ../../node_modules/preact/hooks/dist/hooks.module.js + var t2; + var r2; + var u2; + var i2; + var o2 = 0; + var f2 = []; + var c2 = []; + var e2 = l.__b; + var a2 = l.__r; + var v2 = l.diffed; + var l2 = l.__c; + var m2 = l.unmount; + function d2(t3, u3) { + l.__h && l.__h(r2, t3, o2 || u3), o2 = 0; + var i3 = r2.__H || (r2.__H = { __: [], __h: [] }); + return t3 >= i3.__.length && i3.__.push({ __V: c2 }), i3.__[t3]; + } + function h2(n2) { + return o2 = 1, s2(B, n2); + } + function s2(n2, u3, i3) { + var o3 = d2(t2++, 2); + if (o3.t = n2, !o3.__c && (o3.__ = [i3 ? i3(u3) : B(void 0, u3), function(n3) { + var t3 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t3, n3); + t3 !== r3 && (o3.__N = [r3, o3.__[1]], o3.__c.setState({})); + }], o3.__c = r2, !r2.u)) { + var f3 = function(n3, t3, r3) { + if (!o3.__c.__H) + return true; + var u4 = o3.__c.__H.__.filter(function(n4) { + return n4.__c; + }); + if (u4.every(function(n4) { + return !n4.__N; + })) + return !c3 || c3.call(this, n3, t3, r3); + var i4 = false; + return u4.forEach(function(n4) { + if (n4.__N) { + var t4 = n4.__[0]; + n4.__ = n4.__N, n4.__N = void 0, t4 !== n4.__[0] && (i4 = true); + } + }), !(!i4 && o3.__c.props === n3) && (!c3 || c3.call(this, n3, t3, r3)); + }; + r2.u = true; + var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate; + r2.componentWillUpdate = function(n3, t3, r3) { + if (this.__e) { + var u4 = c3; + c3 = void 0, f3(n3, t3, r3), c3 = u4; + } + e3 && e3.call(this, n3, t3, r3); + }, r2.shouldComponentUpdate = f3; + } + return o3.__N || o3.__; + } + function p2(u3, i3) { + var o3 = d2(t2++, 3); + !l.__s && z2(o3.__H, i3) && (o3.__ = u3, o3.i = i3, r2.__H.__h.push(o3)); + } + function F2(n2, r3) { + var u3 = d2(t2++, 7); + return z2(u3.__H, r3) ? (u3.__V = n2(), u3.i = r3, u3.__h = n2, u3.__V) : u3.__; + } + function T2(n2, t3) { + return o2 = 8, F2(function() { + return n2; + }, t3); + } + function q2(n2) { + var u3 = r2.context[n2.__c], i3 = d2(t2++, 9); + return i3.c = n2, u3 ? (null == i3.__ && (i3.__ = true, u3.sub(r2)), u3.props.value) : n2.__; + } + function b2() { + for (var t3; t3 = f2.shift(); ) + if (t3.__P && t3.__H) + try { + t3.__H.__h.forEach(k2), t3.__H.__h.forEach(w2), t3.__H.__h = []; + } catch (r3) { + t3.__H.__h = [], l.__e(r3, t3.__v); + } + } + l.__b = function(n2) { + r2 = null, e2 && e2(n2); + }, l.__r = function(n2) { + a2 && a2(n2), t2 = 0; + var i3 = (r2 = n2.__c).__H; + i3 && (u2 === r2 ? (i3.__h = [], r2.__h = [], i3.__.forEach(function(n3) { + n3.__N && (n3.__ = n3.__N), n3.__V = c2, n3.__N = n3.i = void 0; + })) : (i3.__h.forEach(k2), i3.__h.forEach(w2), i3.__h = [], t2 = 0)), u2 = r2; + }, l.diffed = function(t3) { + v2 && v2(t3); + var o3 = t3.__c; + o3 && o3.__H && (o3.__H.__h.length && (1 !== f2.push(o3) && i2 === l.requestAnimationFrame || ((i2 = l.requestAnimationFrame) || j2)(b2)), o3.__H.__.forEach(function(n2) { + n2.i && (n2.__H = n2.i), n2.__V !== c2 && (n2.__ = n2.__V), n2.i = void 0, n2.__V = c2; + })), u2 = r2 = null; + }, l.__c = function(t3, r3) { + r3.some(function(t4) { + try { + t4.__h.forEach(k2), t4.__h = t4.__h.filter(function(n2) { + return !n2.__ || w2(n2); + }); + } catch (u3) { + r3.some(function(n2) { + n2.__h && (n2.__h = []); + }), r3 = [], l.__e(u3, t4.__v); + } + }), l2 && l2(t3, r3); + }, l.unmount = function(t3) { + m2 && m2(t3); + var r3, u3 = t3.__c; + u3 && u3.__H && (u3.__H.__.forEach(function(n2) { + try { + k2(n2); + } catch (n3) { + r3 = n3; + } + }), u3.__H = void 0, r3 && l.__e(r3, u3.__v)); + }; + var g2 = "function" == typeof requestAnimationFrame; + function j2(n2) { + var t3, r3 = function() { + clearTimeout(u3), g2 && cancelAnimationFrame(t3), setTimeout(n2); + }, u3 = setTimeout(r3, 100); + g2 && (t3 = requestAnimationFrame(r3)); + } + function k2(n2) { + var t3 = r2, u3 = n2.__c; + "function" == typeof u3 && (n2.__c = void 0, u3()), r2 = t3; + } + function w2(n2) { + var t3 = r2; + n2.__c = n2.__(), r2 = t3; + } + function z2(n2, t3) { + return !n2 || n2.length !== t3.length || t3.some(function(t4, r3) { + return t4 !== n2[r3]; + }); + } + function B(n2, t3) { + return "function" == typeof t3 ? t3(n2) : t3; + } + + // shared/components/EnvironmentProvider.js + var EnvironmentContext = F({ + isReducedMotion: false, + isDarkMode: false, + debugState: false, + injectName: ( + /** @type {import('../environment').Environment['injectName']} */ + "windows" + ), + willThrow: false + }); + var THEME_QUERY = "(prefers-color-scheme: dark)"; + var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)"; + function EnvironmentProvider({ children, debugState, willThrow = false, injectName = "windows" }) { + const [theme, setTheme] = h2(window.matchMedia(THEME_QUERY).matches ? "dark" : "light"); + const [isReducedMotion, setReducedMotion] = h2(window.matchMedia(REDUCED_MOTION_QUERY).matches); + p2(() => { + const mediaQueryList = window.matchMedia(THEME_QUERY); + const listener = (e3) => setTheme(e3.matches ? "dark" : "light"); + mediaQueryList.addEventListener("change", listener); + return () => mediaQueryList.removeEventListener("change", listener); + }, []); + p2(() => { + const mediaQueryList = window.matchMedia(REDUCED_MOTION_QUERY); + const listener = (e3) => setter(e3.matches); + mediaQueryList.addEventListener("change", listener); + setter(mediaQueryList.matches); + function setter(value) { + document.documentElement.dataset.reducedMotion = String(value); + setReducedMotion(value); + } + window.addEventListener("toggle-reduced-motion", () => { + setter(true); + }); + return () => mediaQueryList.removeEventListener("change", listener); + }, []); + return /* @__PURE__ */ y(EnvironmentContext.Provider, { value: { + isReducedMotion, + debugState, + isDarkMode: theme === "dark", + injectName, + willThrow + } }, children); + } + function UpdateEnvironment({ search }) { + p2(() => { + const params = new URLSearchParams(search); + if (params.has("reduced-motion")) { + setTimeout(() => { + window.dispatchEvent(new CustomEvent("toggle-reduced-motion")); + }, 0); + } + }, [search]); + return null; + } + + // shared/components/Fallback/Fallback.module.css + var Fallback_default = { + fallback: "Fallback_fallback" + }; + + // shared/components/Fallback/Fallback.jsx + function Fallback({ showDetails }) { + return /* @__PURE__ */ y("div", { class: Fallback_default.fallback }, /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", null, "Something went wrong!"), showDetails && /* @__PURE__ */ y("p", null, "Please check logs for a message called ", /* @__PURE__ */ y("code", null, "reportPageException")))); + } + + // shared/components/ErrorBoundary.js + var ErrorBoundary = class extends b { + /** + * @param {{didCatch: (params: {error: Error; info: any}) => void}} props + */ + constructor(props) { + super(props); + this.state = { hasError: false }; + } + static getDerivedStateFromError() { + return { hasError: true }; + } + componentDidCatch(error, info) { + console.error(error); + console.log(info); + this.props.didCatch({ error, info }); + } + render() { + if (this.state.hasError) { + return this.props.fallback; + } + return this.props.children; + } + }; + + // pages/new-tab/app/settings.provider.js + var SettingsContext = F( + /** @type {{settings: import("./settings.js").Settings}} */ + {} + ); + function SettingsProvider({ settings, children }) { + return /* @__PURE__ */ y(SettingsContext.Provider, { value: { settings } }, children); + } + + // shared/translations.js + function apply(subject, replacements, textLength = 1) { + if (typeof subject !== "string" || subject.length === 0) + return ""; + let out = subject; + if (replacements) { + for (let [name, value] of Object.entries(replacements)) { + if (typeof value !== "string") + value = ""; + out = out.replaceAll(`{${name}}`, value); + } + } + if (textLength !== 1 && textLength > 0 && textLength <= 2) { + const targetLen = Math.ceil(out.length * textLength); + const target = Math.ceil(textLength); + const combined = out.repeat(target); + return combined.slice(0, targetLen); + } + return out; + } + + // shared/components/TranslationsProvider.js + var TranslationContext = F({ + /** @type {LocalTranslationFn} */ + t: () => { + throw new Error("must implement"); + } + }); + function TranslationProvider({ children, translationObject, fallback, textLength = 1 }) { + function t3(inputKey, replacements) { + const subject = translationObject?.[inputKey]?.title || fallback?.[inputKey]?.title; + return apply(subject, replacements, textLength); + } + return /* @__PURE__ */ y(TranslationContext.Provider, { value: { t: t3 } }, children); + } + + // pages/new-tab/src/locales/en/newtab.json + var newtab_default = { + smartling: { + string_format: "icu", + translate_paths: [ + { + path: "*/title", + key: "{*}/title", + instruction: "*/note" + } + ] + }, + helloWorld: { + title: "Hello world!", + note: "here!" + } + }; + + // pages/new-tab/app/types.js + var MessagingContext = F( + /** @type {import("../src/js/index.js").NewTabPage} */ + {} + ); + + // pages/new-tab/app/widget-list/widget-config.js + var WidgetConfigAPI = class { + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {WidgetConfig} initialData - Initial widget configuration. + */ + constructor(ntp, initialData) { + this.ntp = ntp; + this.manager = new WidgetConfigManager(ntp, initialData); + } + /** + * @param {(data: WidgetConfig) => void} cb + * @return {() => void} - call this returned method to dispose of the subscription + */ + onUpdate(cb) { + const controller = new AbortController(); + this.manager.eventTarget.addEventListener(this.manager.DATA_CHANGE_EVT, (evt) => { + cb(evt.detail); + }, { signal: controller.signal }); + return () => controller.abort(); + } + /** + * @param {string} id + */ + show(id) { + const next = this.manager.inMemoryData.map((w3) => { + if (w3.id === id) + return { ...w3, visibility: ( + /** @type {const} */ + "visible" + ) }; + return w3; + }); + this.manager.update(next); + } + /** + * @param {string} id + */ + hide(id) { + const next = this.manager.inMemoryData.map((w3) => { + if (w3.id === id) + return { ...w3, visibility: ( + /** @type {const} */ + "hidden" + ) }; + return w3; + }); + this.manager.update(next); + } + }; + var WidgetConfigManager = class { + debounceTimer = null; + eventTarget = new EventTarget(); + DATA_CHANGE_EVT = "dataChanged"; + DEBOUNCE_TIME_MS = 200; + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {WidgetConfig} initialData - Initial widget configuration. + */ + constructor(ntp, initialData) { + this.ntp = ntp; + this.inMemoryData = initialData; + this.setupSubscriptionStream(); + } + /** + * Sets up the subscription stream from the data feed, which behaves like an input source + * that updates in-memory data but does not trigger persistence. + */ + setupSubscriptionStream() { + this.ntp.messaging.subscribe("onWidgetConfigUpdated", (newData) => { + this.updateInMemoryData(newData.widgetConfig, "subscription"); + }); + } + /** + * Manually trigger an update, for example, from a UI element. + * @param {WidgetConfig} newData - The new widget configuration to update. + */ + update(newData) { + this.updateInMemoryData(newData, "manual"); + } + /** + * Updates the in-memory data and triggers persistence if the source is a manual update. + * This method centralizes all state updates. + * @param {WidgetConfig} newData - The new widget configuration to update in memory. + * @param {'manual' | 'subscription'} source - The source of the update. Either 'subscription' or 'manual'. + */ + updateInMemoryData(newData, source) { + this.log(`Updating in-memory data from '${source}:'`, newData); + this.inMemoryData = structuredClone(newData); + this.broadcastChange(); + if (source === "manual") { + this.clearDebounceTimer(); + this.debounceTimer = /** @type {any} */ + setTimeout(() => { + this.persist(); + }, this.DEBOUNCE_TIME_MS); + } + } + /** + * Clears the debounce timer if it exists, simulating the switchMap behavior. + */ + clearDebounceTimer() { + if (this.debounceTimer) { + this.log("Clearing previous debounce timer."); + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + } + } + /** + * Broadcasts the current state to external subscribers using EventTarget. + */ + broadcastChange() { + this.log("Broadcasting change to external listeners:", this.inMemoryData); + this.eventTarget.dispatchEvent(new CustomEvent(this.DATA_CHANGE_EVT, { detail: this.inMemoryData })); + } + /** + * Persists the current in-memory widget configuration state to the internal data feed. + */ + persist() { + this.log("will persist data to backend:", this.inMemoryData); + this.ntp.messaging.notify("setWidgetConfig", { widgetConfig: this.inMemoryData }); + } + /** + * Logs messages to the console for tracing internal state updates. + * @param {string} message - The message to log. + * @param {any} data - The associated data for the message. + */ + log(message, data = null) { + console.log(`[WidgetConfigManager] ${message}`, data); + } + }; + + // pages/new-tab/app/widget-list/widget-config.provider.js + var WidgetConfigContext = F({ + /** @type {WidgetList} */ + widgets: [], + /** @type {WidgetConfig} */ + widgetConfig: [], + /** @type {(name:string) => void} */ + hide: () => { + }, + /** @type {(name:string) => void} */ + show: () => { + } + }); + var WidgetConfigDispatchContext = F({ + dispatch: null + }); + function WidgetConfigProvider(props) { + const [data, setData] = h2(props.widgetConfig); + p2(() => { + const unsub = props.api.onUpdate((widgetConfig) => { + setData(widgetConfig); + }); + return () => unsub(); + }, [props.api]); + function hide(name) { + console.log("will hide", name); + props.api.hide(name); + } + function show(name) { + console.log("will show", name); + props.api.show(name); + } + return /* @__PURE__ */ y(WidgetConfigContext.Provider, { value: { + // this field is static for the lifespan of the page + widgets: props.widgets, + // this will be updated via subscriptions + widgetConfig: data, + hide, + show + } }, props.children); + } + var WidgetVisibilityContext = F({ + visibility: ( + /** @type {WidgetConfigItem['visibility']} */ + "visible" + ), + id: ( + /** @type {WidgetConfigItem['id']} */ + "" + ), + toggle: () => { + } + }); + function useVisibility() { + return q2(WidgetVisibilityContext); + } + function WidgetVisibilityProvider(props) { + const { widgetConfig, show, hide } = q2(WidgetConfigContext); + const toggle = T2(() => { + const matching = widgetConfig.find((x2) => x2.id === props.id); + if (matching?.visibility === "visible") { + hide(props.id); + } else { + show(props.id); + } + }, [props.id, widgetConfig]); + return /* @__PURE__ */ y(WidgetVisibilityContext.Provider, { value: { + visibility: props.visibility, + id: props.id, + toggle + } }, props.children); + } + + // pages/new-tab/app/widget-list/WidgetList.js + var widgetMap = { + favorites: () => /* @__PURE__ */ y(Favorites, null), + privacyStats: () => /* @__PURE__ */ y(PrivacyStats, null) + }; + function WidgetList() { + const { widgets, widgetConfig } = q2(WidgetConfigContext); + return /* @__PURE__ */ y("div", null, widgets.map((widget) => { + const matchingConfig = widgetConfig.find((item) => item.id === widget.id); + if (!matchingConfig) { + console.warn("missing config for widget: ", widget); + return null; + } + return /* @__PURE__ */ y(g, { key: widget.id }, /* @__PURE__ */ y( + WidgetVisibilityProvider, + { + visibility: matchingConfig.visibility, + id: matchingConfig.id + }, + widgetMap[widget.id]?.() + )); + })); + } + function Favorites() { + const { visibility, id, toggle } = useVisibility(); + return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Favorites Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Favorites")); + } + function PrivacyStats() { + const { visibility, id, toggle } = useVisibility(); + return /* @__PURE__ */ y("div", null, /* @__PURE__ */ y("p", { style: { opacity: visibility === "visible" ? "1" : "0.2" } }, "Privacy Stats Component"), /* @__PURE__ */ y("code", null, /* @__PURE__ */ y("b", null, id, " visibility: ", visibility)), " ", /* @__PURE__ */ y("button", { type: "button", onClick: toggle }, "Toggle Privacy Stats")); + } + + // pages/new-tab/app/settings.js + var Settings = class _Settings { + /** + * @param {object} params + * @param {{name: ImportMeta['platform']}} [params.platform] + */ + constructor({ + platform = { name: "windows" } + }) { + this.platform = platform; + } + withPlatformName(name) { + const valid = ["windows", "macos", "ios", "android"]; + if (valid.includes( + /** @type {any} */ + name + )) { + return new _Settings({ + ...this, + platform: { name } + }); + } + return this; + } + }; + + // pages/new-tab/app/index.js + async function init(messaging2, baseEnvironment2) { + const init2 = await messaging2.init(); + if (!Array.isArray(init2.widgets)) { + throw new Error("missing critical initialSetup.widgets array"); + } + if (!Array.isArray(init2.widgetConfig)) { + throw new Error("missing critical initialSetup.widgetConfig array"); + } + const widgetConfigAPI = new WidgetConfigAPI(messaging2, init2.widgetConfig); + const environment = baseEnvironment2.withEnv(init2.env).withLocale(init2.locale).withLocale(baseEnvironment2.urlParams.get("locale")).withTextLength(baseEnvironment2.urlParams.get("textLength")).withDisplay(baseEnvironment2.urlParams.get("display")); + console.log("environment:", environment); + console.log("locale:", environment.locale); + const strings = environment.locale === "en" ? newtab_default : await fetch(`./locales/${environment.locale}/new-tab.json`).then((x2) => x2.json()).catch((e3) => { + console.error("Could not load locale", environment.locale, e3); + return newtab_default; + }); + const settings = new Settings({}).withPlatformName(baseEnvironment2.injectName).withPlatformName(init2.platform?.name).withPlatformName(baseEnvironment2.urlParams.get("platform")); + const didCatch = (error) => { + const message = error?.message || error?.error || "unknown"; + messaging2.reportPageException({ message }); + }; + const root = document.querySelector("#app"); + if (!root) + throw new Error("could not render, root element missing"); + q( + /* @__PURE__ */ y( + EnvironmentProvider, + { + debugState: environment.debugState, + injectName: environment.injectName, + willThrow: environment.willThrow + }, + /* @__PURE__ */ y(ErrorBoundary, { didCatch, fallback: /* @__PURE__ */ y(Fallback, { showDetails: environment.env === "development" }) }, /* @__PURE__ */ y(UpdateEnvironment, { search: window.location.search }), /* @__PURE__ */ y(MessagingContext.Provider, { value: messaging2 }, /* @__PURE__ */ y(SettingsProvider, { settings }, /* @__PURE__ */ y(TranslationProvider, { translationObject: strings, fallback: strings, textLength: environment.textLength }, /* @__PURE__ */ y(WidgetConfigProvider, { api: widgetConfigAPI, widgetConfig: init2.widgetConfig, widgets: init2.widgets }, /* @__PURE__ */ y(App, null, /* @__PURE__ */ y(WidgetList, null))))))) + ), + root + ); + } + + // ../messaging/lib/windows.js + var WindowsMessagingTransport = class { + /** + * @param {WindowsMessagingConfig} config + * @param {import('../index.js').MessagingContext} messagingContext + * @internal + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + this.globals = { + window, + JSONparse: window.JSON.parse, + JSONstringify: window.JSON.stringify, + Promise: window.Promise, + Error: window.Error, + String: window.String + }; + for (const [methodName, fn] of Object.entries(this.config.methods)) { + if (typeof fn !== "function") { + throw new Error("cannot create WindowsMessagingTransport, missing the method: " + methodName); + } + } + } + /** + * @param {import('../index.js').NotificationMessage} msg + */ + notify(msg) { + const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {})); + const notification = WindowsNotification.fromNotification(msg, data); + this.config.methods.postMessage(notification); + } + /** + * @param {import('../index.js').RequestMessage} msg + * @param {{signal?: AbortSignal}} opts + * @return {Promise} + */ + request(msg, opts = {}) { + const data = this.globals.JSONparse(this.globals.JSONstringify(msg.params || {})); + const outgoing = WindowsRequestMessage.fromRequest(msg, data); + this.config.methods.postMessage(outgoing); + const comparator = (eventData) => { + return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.id === msg.id; + }; + function isMessageResponse(data2) { + if ("result" in data2) + return true; + if ("error" in data2) + return true; + return false; + } + return new this.globals.Promise((resolve, reject) => { + try { + this._subscribe(comparator, opts, (value, unsubscribe) => { + unsubscribe(); + if (!isMessageResponse(value)) { + console.warn("unknown response type", value); + return reject(new this.globals.Error("unknown response")); + } + if (value.result) { + return resolve(value.result); + } + const message = this.globals.String(value.error?.message || "unknown error"); + reject(new this.globals.Error(message)); + }); + } catch (e3) { + reject(e3); + } + }); + } + /** + * @param {import('../index.js').Subscription} msg + * @param {(value: unknown | undefined) => void} callback + */ + subscribe(msg, callback) { + const comparator = (eventData) => { + return eventData.featureName === msg.featureName && eventData.context === msg.context && eventData.subscriptionName === msg.subscriptionName; + }; + const cb = (eventData) => { + return callback(eventData.params); + }; + return this._subscribe(comparator, {}, cb); + } + /** + * @typedef {import('../index.js').MessageResponse | import('../index.js').SubscriptionEvent} Incoming + */ + /** + * @param {(eventData: any) => boolean} comparator + * @param {{signal?: AbortSignal}} options + * @param {(value: Incoming, unsubscribe: (()=>void)) => void} callback + * @internal + */ + _subscribe(comparator, options, callback) { + if (options?.signal?.aborted) { + throw new DOMException("Aborted", "AbortError"); + } + let teardown; + const idHandler = (event) => { + if (this.messagingContext.env === "production") { + if (event.origin !== null && event.origin !== void 0) { + console.warn("ignoring because evt.origin is not `null` or `undefined`"); + return; + } + } + if (!event.data) { + console.warn("data absent from message"); + return; + } + if (comparator(event.data)) { + if (!teardown) + throw new Error("unreachable"); + callback(event.data, teardown); + } + }; + const abortHandler = () => { + teardown?.(); + throw new DOMException("Aborted", "AbortError"); + }; + this.config.methods.addEventListener("message", idHandler); + options?.signal?.addEventListener("abort", abortHandler); + teardown = () => { + this.config.methods.removeEventListener("message", idHandler); + options?.signal?.removeEventListener("abort", abortHandler); + }; + return () => { + teardown?.(); + }; + } + }; + var WindowsMessagingConfig = class { + /** + * @param {object} params + * @param {WindowsInteropMethods} params.methods + * @internal + */ + constructor(params) { + this.methods = params.methods; + this.platform = "windows"; + } + }; + var WindowsNotification = class { + /** + * @param {object} params + * @param {string} params.Feature + * @param {string} params.SubFeatureName + * @param {string} params.Name + * @param {Record} [params.Data] + * @internal + */ + constructor(params) { + this.Feature = params.Feature; + this.SubFeatureName = params.SubFeatureName; + this.Name = params.Name; + this.Data = params.Data; + } + /** + * Helper to convert a {@link NotificationMessage} to a format that Windows can support + * @param {NotificationMessage} notification + * @returns {WindowsNotification} + */ + static fromNotification(notification, data) { + const output = { + Data: data, + Feature: notification.context, + SubFeatureName: notification.featureName, + Name: notification.method + }; + return output; + } + }; + var WindowsRequestMessage = class { + /** + * @param {object} params + * @param {string} params.Feature + * @param {string} params.SubFeatureName + * @param {string} params.Name + * @param {Record} [params.Data] + * @param {string} [params.Id] + * @internal + */ + constructor(params) { + this.Feature = params.Feature; + this.SubFeatureName = params.SubFeatureName; + this.Name = params.Name; + this.Data = params.Data; + this.Id = params.Id; + } + /** + * Helper to convert a {@link RequestMessage} to a format that Windows can support + * @param {RequestMessage} msg + * @param {Record} data + * @returns {WindowsRequestMessage} + */ + static fromRequest(msg, data) { + const output = { + Data: data, + Feature: msg.context, + SubFeatureName: msg.featureName, + Name: msg.method, + Id: msg.id + }; + return output; + } + }; + + // ../messaging/schema.js + var RequestMessage = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.method + * @param {string} params.id + * @param {Record} [params.params] + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.method = params.method; + this.id = params.id; + this.params = params.params; + } + }; + var NotificationMessage = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.method + * @param {Record} [params.params] + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.method = params.method; + this.params = params.params; + } + }; + var Subscription = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {string} params.subscriptionName + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.subscriptionName = params.subscriptionName; + } + }; + function isResponseFor(request, data) { + if ("result" in data) { + return data.featureName === request.featureName && data.context === request.context && data.id === request.id; + } + if ("error" in data) { + if ("message" in data.error) { + return true; + } + } + return false; + } + function isSubscriptionEventFor(sub, data) { + if ("subscriptionName" in data) { + return data.featureName === sub.featureName && data.context === sub.context && data.subscriptionName === sub.subscriptionName; + } + return false; + } + + // ../messaging/lib/webkit.js + var WebkitMessagingTransport = class { + /** + * @param {WebkitMessagingConfig} config + * @param {import('../index.js').MessagingContext} messagingContext + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + this.globals = captureGlobals(); + if (!this.config.hasModernWebkitAPI) { + this.captureWebkitHandlers(this.config.webkitMessageHandlerNames); + } + } + /** + * Sends message to the webkit layer (fire and forget) + * @param {String} handler + * @param {*} data + * @internal + */ + wkSend(handler, data = {}) { + if (!(handler in this.globals.window.webkit.messageHandlers)) { + throw new MissingHandler(`Missing webkit handler: '${handler}'`, handler); + } + if (!this.config.hasModernWebkitAPI) { + const outgoing = { + ...data, + messageHandling: { + ...data.messageHandling, + secret: this.config.secret + } + }; + if (!(handler in this.globals.capturedWebkitHandlers)) { + throw new MissingHandler(`cannot continue, method ${handler} not captured on macos < 11`, handler); + } else { + return this.globals.capturedWebkitHandlers[handler](outgoing); + } + } + return this.globals.window.webkit.messageHandlers[handler].postMessage?.(data); + } + /** + * Sends message to the webkit layer and waits for the specified response + * @param {String} handler + * @param {import('../index.js').RequestMessage} data + * @returns {Promise<*>} + * @internal + */ + async wkSendAndWait(handler, data) { + if (this.config.hasModernWebkitAPI) { + const response = await this.wkSend(handler, data); + return this.globals.JSONparse(response || "{}"); + } + try { + const randMethodName = this.createRandMethodName(); + const key = await this.createRandKey(); + const iv = this.createRandIv(); + const { + ciphertext, + tag + } = await new this.globals.Promise((resolve) => { + this.generateRandomMethod(randMethodName, resolve); + data.messageHandling = new SecureMessagingParams({ + methodName: randMethodName, + secret: this.config.secret, + key: this.globals.Arrayfrom(key), + iv: this.globals.Arrayfrom(iv) + }); + this.wkSend(handler, data); + }); + const cipher = new this.globals.Uint8Array([...ciphertext, ...tag]); + const decrypted = await this.decrypt(cipher, key, iv); + return this.globals.JSONparse(decrypted || "{}"); + } catch (e3) { + if (e3 instanceof MissingHandler) { + throw e3; + } else { + console.error("decryption failed", e3); + console.error(e3); + return { error: e3 }; + } + } + } + /** + * @param {import('../index.js').NotificationMessage} msg + */ + notify(msg) { + this.wkSend(msg.context, msg); + } + /** + * @param {import('../index.js').RequestMessage} msg + */ + async request(msg) { + const data = await this.wkSendAndWait(msg.context, msg); + if (isResponseFor(msg, data)) { + if (data.result) { + return data.result || {}; + } + if (data.error) { + throw new Error(data.error.message); + } + } + throw new Error("an unknown error occurred"); + } + /** + * Generate a random method name and adds it to the global scope + * The native layer will use this method to send the response + * @param {string | number} randomMethodName + * @param {Function} callback + * @internal + */ + generateRandomMethod(randomMethodName, callback) { + this.globals.ObjectDefineProperty(this.globals.window, randomMethodName, { + enumerable: false, + // configurable, To allow for deletion later + configurable: true, + writable: false, + /** + * @param {any[]} args + */ + value: (...args) => { + callback(...args); + delete this.globals.window[randomMethodName]; + } + }); + } + /** + * @internal + * @return {string} + */ + randomString() { + return "" + this.globals.getRandomValues(new this.globals.Uint32Array(1))[0]; + } + /** + * @internal + * @return {string} + */ + createRandMethodName() { + return "_" + this.randomString(); + } + /** + * @type {{name: string, length: number}} + * @internal + */ + algoObj = { + name: "AES-GCM", + length: 256 + }; + /** + * @returns {Promise} + * @internal + */ + async createRandKey() { + const key = await this.globals.generateKey(this.algoObj, true, ["encrypt", "decrypt"]); + const exportedKey = await this.globals.exportKey("raw", key); + return new this.globals.Uint8Array(exportedKey); + } + /** + * @returns {Uint8Array} + * @internal + */ + createRandIv() { + return this.globals.getRandomValues(new this.globals.Uint8Array(12)); + } + /** + * @param {BufferSource} ciphertext + * @param {BufferSource} key + * @param {Uint8Array} iv + * @returns {Promise} + * @internal + */ + async decrypt(ciphertext, key, iv) { + const cryptoKey = await this.globals.importKey("raw", key, "AES-GCM", false, ["decrypt"]); + const algo = { + name: "AES-GCM", + iv + }; + const decrypted = await this.globals.decrypt(algo, cryptoKey, ciphertext); + const dec = new this.globals.TextDecoder(); + return dec.decode(decrypted); + } + /** + * When required (such as on macos 10.x), capture the `postMessage` method on + * each webkit messageHandler + * + * @param {string[]} handlerNames + */ + captureWebkitHandlers(handlerNames) { + const handlers = window.webkit.messageHandlers; + if (!handlers) + throw new MissingHandler("window.webkit.messageHandlers was absent", "all"); + for (const webkitMessageHandlerName of handlerNames) { + if (typeof handlers[webkitMessageHandlerName]?.postMessage === "function") { + const original = handlers[webkitMessageHandlerName]; + const bound = handlers[webkitMessageHandlerName].postMessage?.bind(original); + this.globals.capturedWebkitHandlers[webkitMessageHandlerName] = bound; + delete handlers[webkitMessageHandlerName].postMessage; + } + } + } + /** + * @param {import('../index.js').Subscription} msg + * @param {(value: unknown) => void} callback + */ + subscribe(msg, callback) { + if (msg.subscriptionName in this.globals.window) { + throw new this.globals.Error(`A subscription with the name ${msg.subscriptionName} already exists`); + } + this.globals.ObjectDefineProperty(this.globals.window, msg.subscriptionName, { + enumerable: false, + configurable: true, + writable: false, + value: (data) => { + if (data && isSubscriptionEventFor(msg, data)) { + callback(data.params); + } else { + console.warn("Received a message that did not match the subscription", data); + } + } + }); + return () => { + this.globals.ReflectDeleteProperty(this.globals.window, msg.subscriptionName); + }; + } + }; + var WebkitMessagingConfig = class { + /** + * @param {object} params + * @param {boolean} params.hasModernWebkitAPI + * @param {string[]} params.webkitMessageHandlerNames + * @param {string} params.secret + * @internal + */ + constructor(params) { + this.hasModernWebkitAPI = params.hasModernWebkitAPI; + this.webkitMessageHandlerNames = params.webkitMessageHandlerNames; + this.secret = params.secret; + } + }; + var SecureMessagingParams = class { + /** + * @param {object} params + * @param {string} params.methodName + * @param {string} params.secret + * @param {number[]} params.key + * @param {number[]} params.iv + */ + constructor(params) { + this.methodName = params.methodName; + this.secret = params.secret; + this.key = params.key; + this.iv = params.iv; + } + }; + function captureGlobals() { + const globals = { + window, + getRandomValues: window.crypto.getRandomValues.bind(window.crypto), + TextEncoder, + TextDecoder, + Uint8Array, + Uint16Array, + Uint32Array, + JSONstringify: window.JSON.stringify, + JSONparse: window.JSON.parse, + Arrayfrom: window.Array.from, + Promise: window.Promise, + Error: window.Error, + ReflectDeleteProperty: window.Reflect.deleteProperty.bind(window.Reflect), + ObjectDefineProperty: window.Object.defineProperty, + addEventListener: window.addEventListener.bind(window), + /** @type {Record} */ + capturedWebkitHandlers: {} + }; + if (isSecureContext) { + globals.generateKey = window.crypto.subtle.generateKey.bind(window.crypto.subtle); + globals.exportKey = window.crypto.subtle.exportKey.bind(window.crypto.subtle); + globals.importKey = window.crypto.subtle.importKey.bind(window.crypto.subtle); + globals.encrypt = window.crypto.subtle.encrypt.bind(window.crypto.subtle); + globals.decrypt = window.crypto.subtle.decrypt.bind(window.crypto.subtle); + } + return globals; + } + + // ../messaging/lib/android.js + var AndroidMessagingTransport = class { + /** + * @param {AndroidMessagingConfig} config + * @param {MessagingContext} messagingContext + * @internal + */ + constructor(config, messagingContext) { + this.messagingContext = messagingContext; + this.config = config; + } + /** + * @param {NotificationMessage} msg + */ + notify(msg) { + try { + this.config.sendMessageThrows?.(JSON.stringify(msg)); + } catch (e3) { + console.error(".notify failed", e3); + } + } + /** + * @param {RequestMessage} msg + * @return {Promise} + */ + request(msg) { + return new Promise((resolve, reject) => { + const unsub = this.config.subscribe(msg.id, handler); + try { + this.config.sendMessageThrows?.(JSON.stringify(msg)); + } catch (e3) { + unsub(); + reject(new Error("request failed to send: " + e3.message || "unknown error")); + } + function handler(data) { + if (isResponseFor(msg, data)) { + if (data.result) { + resolve(data.result || {}); + return unsub(); + } + if (data.error) { + reject(new Error(data.error.message)); + return unsub(); + } + unsub(); + throw new Error("unreachable: must have `result` or `error` key by this point"); + } + } + }); + } + /** + * @param {Subscription} msg + * @param {(value: unknown | undefined) => void} callback + */ + subscribe(msg, callback) { + const unsub = this.config.subscribe(msg.subscriptionName, (data) => { + if (isSubscriptionEventFor(msg, data)) { + callback(data.params || {}); + } + }); + return () => { + unsub(); + }; + } + }; + var AndroidMessagingConfig = class { + /** @type {(json: string, secret: string) => void} */ + _capturedHandler; + /** + * @param {object} params + * @param {Record} params.target + * @param {boolean} params.debug + * @param {string} params.messageSecret - a secret to ensure that messages are only + * processed by the correct handler + * @param {string} params.javascriptInterface - the name of the javascript interface + * registered on the native side + * @param {string} params.messageCallback - the name of the callback that the native + * side will use to send messages back to the javascript side + */ + constructor(params) { + this.target = params.target; + this.debug = params.debug; + this.javascriptInterface = params.javascriptInterface; + this.messageSecret = params.messageSecret; + this.messageCallback = params.messageCallback; + this.listeners = new globalThis.Map(); + this._captureGlobalHandler(); + this._assignHandlerMethod(); + } + /** + * The transport can call this to transmit a JSON payload along with a secret + * to the native Android handler. + * + * Note: This can throw - it's up to the transport to handle the error. + * + * @type {(json: string) => void} + * @throws + * @internal + */ + sendMessageThrows(json) { + this._capturedHandler(json, this.messageSecret); + } + /** + * A subscription on Android is just a named listener. All messages from + * android -> are delivered through a single function, and this mapping is used + * to route the messages to the correct listener. + * + * Note: Use this to implement request->response by unsubscribing after the first + * response. + * + * @param {string} id + * @param {(msg: MessageResponse | SubscriptionEvent) => void} callback + * @returns {() => void} + * @internal + */ + subscribe(id, callback) { + this.listeners.set(id, callback); + return () => { + this.listeners.delete(id); + }; + } + /** + * Accept incoming messages and try to deliver it to a registered listener. + * + * This code is defensive to prevent any single handler from affecting another if + * it throws (producer interference). + * + * @param {MessageResponse | SubscriptionEvent} payload + * @internal + */ + _dispatch(payload) { + if (!payload) + return this._log("no response"); + if ("id" in payload) { + if (this.listeners.has(payload.id)) { + this._tryCatch(() => this.listeners.get(payload.id)?.(payload)); + } else { + this._log("no listeners for ", payload); + } + } + if ("subscriptionName" in payload) { + if (this.listeners.has(payload.subscriptionName)) { + this._tryCatch(() => this.listeners.get(payload.subscriptionName)?.(payload)); + } else { + this._log("no subscription listeners for ", payload); + } + } + } + /** + * + * @param {(...args: any[]) => any} fn + * @param {string} [context] + */ + _tryCatch(fn, context = "none") { + try { + return fn(); + } catch (e3) { + if (this.debug) { + console.error("AndroidMessagingConfig error:", context); + console.error(e3); + } + } + } + /** + * @param {...any} args + */ + _log(...args) { + if (this.debug) { + console.log("AndroidMessagingConfig", ...args); + } + } + /** + * Capture the global handler and remove it from the global object. + */ + _captureGlobalHandler() { + const { target, javascriptInterface } = this; + if (Object.prototype.hasOwnProperty.call(target, javascriptInterface)) { + this._capturedHandler = target[javascriptInterface].process.bind(target[javascriptInterface]); + delete target[javascriptInterface]; + } else { + this._capturedHandler = () => { + this._log("Android messaging interface not available", javascriptInterface); + }; + } + } + /** + * Assign the incoming handler method to the global object. + * This is the method that Android will call to deliver messages. + */ + _assignHandlerMethod() { + const responseHandler = (providedSecret, response) => { + if (providedSecret === this.messageSecret) { + this._dispatch(response); + } + }; + Object.defineProperty(this.target, this.messageCallback, { + value: responseHandler + }); + } + }; + + // ../messaging/lib/typed-messages.js + function createTypedMessages(base, messaging2) { + const asAny = ( + /** @type {any} */ + messaging2 + ); + return ( + /** @type {BaseClass} */ + asAny + ); + } + + // ../messaging/index.js + var MessagingContext2 = class { + /** + * @param {object} params + * @param {string} params.context + * @param {string} params.featureName + * @param {"production" | "development"} params.env + * @internal + */ + constructor(params) { + this.context = params.context; + this.featureName = params.featureName; + this.env = params.env; + } + }; + var Messaging = class { + /** + * @param {MessagingContext} messagingContext + * @param {MessagingConfig} config + */ + constructor(messagingContext, config) { + this.messagingContext = messagingContext; + this.transport = getTransport(config, this.messagingContext); + } + /** + * Send a 'fire-and-forget' message. + * @throws {MissingHandler} + * + * @example + * + * ```ts + * const messaging = new Messaging(config) + * messaging.notify("foo", {bar: "baz"}) + * ``` + * @param {string} name + * @param {Record} [data] + */ + notify(name, data = {}) { + const message = new NotificationMessage({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + method: name, + params: data + }); + this.transport.notify(message); + } + /** + * Send a request, and wait for a response + * @throws {MissingHandler} + * + * @example + * ``` + * const messaging = new Messaging(config) + * const response = await messaging.request("foo", {bar: "baz"}) + * ``` + * + * @param {string} name + * @param {Record} [data] + * @return {Promise} + */ + request(name, data = {}) { + const id = globalThis?.crypto?.randomUUID?.() || name + ".response"; + const message = new RequestMessage({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + method: name, + params: data, + id + }); + return this.transport.request(message); + } + /** + * @param {string} name + * @param {(value: unknown) => void} callback + * @return {() => void} + */ + subscribe(name, callback) { + const msg = new Subscription({ + context: this.messagingContext.context, + featureName: this.messagingContext.featureName, + subscriptionName: name + }); + return this.transport.subscribe(msg, callback); + } + }; + var TestTransportConfig = class { + /** + * @param {MessagingTransport} impl + */ + constructor(impl) { + this.impl = impl; + } + }; + var TestTransport = class { + /** + * @param {TestTransportConfig} config + * @param {MessagingContext} messagingContext + */ + constructor(config, messagingContext) { + this.config = config; + this.messagingContext = messagingContext; + } + notify(msg) { + return this.config.impl.notify(msg); + } + request(msg) { + return this.config.impl.request(msg); + } + subscribe(msg, callback) { + return this.config.impl.subscribe(msg, callback); + } + }; + function getTransport(config, messagingContext) { + if (config instanceof WebkitMessagingConfig) { + return new WebkitMessagingTransport(config, messagingContext); + } + if (config instanceof WindowsMessagingConfig) { + return new WindowsMessagingTransport(config, messagingContext); + } + if (config instanceof AndroidMessagingConfig) { + return new AndroidMessagingTransport(config, messagingContext); + } + if (config instanceof TestTransportConfig) { + return new TestTransport(config, messagingContext); + } + throw new Error("unreachable"); + } + var MissingHandler = class extends Error { + /** + * @param {string} message + * @param {string} handlerName + */ + constructor(message, handlerName) { + super(message); + this.handlerName = handlerName; + } + }; + + // shared/create-special-page-messaging.js + function createSpecialPageMessaging(opts) { + const messageContext = new MessagingContext2({ + context: "specialPages", + featureName: opts.pageName, + env: opts.env + }); + try { + if (opts.injectName === "windows") { + const opts2 = new WindowsMessagingConfig({ + methods: { + // @ts-expect-error - not in @types/chrome + postMessage: window.chrome.webview.postMessage, + // @ts-expect-error - not in @types/chrome + addEventListener: window.chrome.webview.addEventListener, + // @ts-expect-error - not in @types/chrome + removeEventListener: window.chrome.webview.removeEventListener + } + }); + return new Messaging(messageContext, opts2); + } else if (opts.injectName === "apple") { + const opts2 = new WebkitMessagingConfig({ + hasModernWebkitAPI: true, + secret: "", + webkitMessageHandlerNames: ["specialPages"] + }); + return new Messaging(messageContext, opts2); + } else if (opts.injectName === "android") { + const opts2 = new AndroidMessagingConfig({ + messageSecret: "duckduckgo-android-messaging-secret", + messageCallback: "messageCallback", + javascriptInterface: messageContext.context, + target: globalThis, + debug: true + }); + return new Messaging(messageContext, opts2); + } + } catch (e3) { + console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); + } + const fallback = opts.mockTransport?.() || new TestTransportConfig({ + /** + * @param {import('@duckduckgo/messaging').NotificationMessage} msg + */ + notify(msg) { + console.log(msg); + }, + /** + * @param {import('@duckduckgo/messaging').RequestMessage} msg + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + request: (msg) => { + console.log(msg); + if (msg.method === "initialSetup") { + return Promise.resolve({ + locale: "en", + env: opts.env + }); + } + return Promise.resolve(null); + }, + /** + * @param {import('@duckduckgo/messaging').SubscriptionEvent} msg + */ + subscribe(msg) { + console.log(msg); + return () => { + console.log("teardown"); + }; + } + }); + return new Messaging(messageContext, fallback); + } + + // shared/environment.js + var Environment = class _Environment { + /** + * @param {object} params + * @param {'app' | 'components'} [params.display] - whether to show the application or component list + * @param {'production' | 'development'} [params.env] - application environment + * @param {URLSearchParams} [params.urlParams] - URL params passed into the page + * @param {ImportMeta['injectName']} [params.injectName] - application platform + * @param {boolean} [params.willThrow] - whether the application will simulate an error + * @param {boolean} [params.debugState] - whether to show debugging UI + * @param {string} [params.locale] - for applications strings + * @param {number} [params.textLength] - what ratio of text should be used. Set a number higher than 1 to have longer strings for testing + */ + constructor({ + env = "production", + urlParams = new URLSearchParams(location.search), + injectName = "windows", + willThrow = urlParams.get("willThrow") === "true", + debugState = urlParams.has("debugState"), + display = "app", + locale = "en", + textLength = 1 + } = {}) { + this.display = display; + this.urlParams = urlParams; + this.injectName = injectName; + this.willThrow = willThrow; + this.debugState = debugState; + this.env = env; + this.locale = locale; + this.textLength = textLength; + } + /** + * @param {string|null|undefined} injectName + * @returns {Environment} + */ + withInjectName(injectName) { + if (!injectName) + return this; + if (!isInjectName(injectName)) + return this; + return new _Environment({ + ...this, + injectName + }); + } + /** + * @param {string|null|undefined} env + * @returns {Environment} + */ + withEnv(env) { + if (!env) + return this; + if (env !== "production" && env !== "development") + return this; + return new _Environment({ + ...this, + env + }); + } + /** + * @param {string|null|undefined} display + * @returns {Environment} + */ + withDisplay(display) { + if (!display) + return this; + if (display !== "app" && display !== "components") + return this; + return new _Environment({ + ...this, + display + }); + } + /** + * @param {string|null|undefined} locale + * @returns {Environment} + */ + withLocale(locale) { + if (!locale) + return this; + if (typeof locale !== "string") + return this; + if (locale.length !== 2) + return this; + return new _Environment({ + ...this, + locale + }); + } + /** + * @param {string|number|null|undefined} length + * @returns {Environment} + */ + withTextLength(length) { + if (!length) + return this; + const num = Number(length); + if (num >= 1 && num <= 2) { + return new _Environment({ + ...this, + textLength: num + }); + } + return this; + } + }; + function isInjectName(input) { + const allowed = ["windows", "apple", "integration", "android"]; + return allowed.includes(input); + } + + // pages/new-tab/src/js/index.js + var NewTabPage = class { + /** + * @param {import("@duckduckgo/messaging").Messaging} messaging + * @param {ImportMeta['injectName']} injectName + */ + constructor(messaging2, injectName) { + this.messaging = createTypedMessages(this, messaging2); + this.injectName = injectName; + } + /** + * @return {Promise} + */ + init() { + return this.messaging.request("initialSetup"); + } + /** + * @param {string} message + */ + reportInitException(message) { + this.messaging.notify("reportInitException", { message }); + } + /** + * This will be sent if the application has loaded, but a client-side error + * has occurred that cannot be recovered from + * @param {{message: string}} params + */ + reportPageException(params) { + this.messaging.notify("reportPageException", params); + } + }; + var baseEnvironment = new Environment().withInjectName("windows").withEnv("production"); + var messaging = createSpecialPageMessaging({ + injectName: "windows", + env: "production", + pageName: "newTabPage", + mockTransport: () => { + if (baseEnvironment.injectName !== "integration") + return null; + if (window.__playwright_01) + return null; + let mock = null; + return mock; + } + }); + var newTabMessaging = new NewTabPage(messaging, "windows"); + init(newTabMessaging, baseEnvironment).catch((e3) => { + console.error(e3); + const msg = typeof e3?.message === "string" ? e3.message : "unknown init error"; + newTabMessaging.reportInitException(msg); + }); +})(); diff --git a/build/windows/pages/new-tab/js/inline.js b/build/windows/pages/new-tab/js/inline.js new file mode 100644 index 000000000..5a71d2623 --- /dev/null +++ b/build/windows/pages/new-tab/js/inline.js @@ -0,0 +1,3 @@ +"use strict"; +(() => { +})(); diff --git a/build/windows/pages/new-tab/js/mock-transport.js b/build/windows/pages/new-tab/js/mock-transport.js new file mode 100644 index 000000000..ca6b115d2 --- /dev/null +++ b/build/windows/pages/new-tab/js/mock-transport.js @@ -0,0 +1,103 @@ +import { TestTransportConfig } from '@duckduckgo/messaging' + +export function mockTransport () { + const channel = new BroadcastChannel('ntp') + + function broadcast () { + setTimeout(() => { + channel.postMessage({ + change: 'ntp.widgetConfig' + }) + }, 100) + } + + /** + * @param {string} name + * @return {Record|null} + */ + function read (name) { + try { + const item = localStorage.getItem(name) + if (!item) return null + console.log('did read from LS', item) + return JSON.parse(item) + } catch (e) { + console.error('Failed to parse initialSetup from localStorage', e) + return null + } + } + + /** + * @param {string} name + * @param {Record} value + */ + function write (name, value) { + try { + localStorage.setItem(name, JSON.stringify(value)) + console.log('✅ did write') + } catch (e) { + console.error('Failed to write', e) + } + } + + return new TestTransportConfig({ + notify (msg) { + switch (msg.method) { + case 'setWidgetConfig': { + if (!msg.params) throw new Error('unreachable') + write('ntp.widgetConfig', msg.params) + broadcast() + return + } + default: { + console.warn('unhandled notification', msg) + } + } + }, + subscribe (sub, cb) { + switch (sub.subscriptionName) { + case 'onWidgetConfigUpdated': { + const controller = new AbortController() + // console.log('sub?', sub, cb); + channel.addEventListener('message', () => { + const values = read('ntp.widgetConfig') + if (values) { + cb(values) + } + }, { signal: controller.signal }) + return () => controller.abort() + } + } + return () => {} + }, + // eslint-ignore-next-line require-await + request (msg) { + switch (msg.method) { + case 'initialSetup': { + const widgetsFromStorage = read('ntp.widgets') || { + widgets: [ + { id: 'favorites' }, + { id: 'privacyStats' } + ] + } + const widgetConfigFromStorage = read('ntp.widgetConfig') || { + widgetConfig: [ + { id: 'favorites', visibility: 'visible' }, + { id: 'privacyStats', visibility: 'visible' } + ] + } + return Promise.resolve({ + widgets: widgetsFromStorage.widgets, + widgetConfig: widgetConfigFromStorage.widgetConfig, + platform: { name: 'integration' }, + env: 'development', + locale: 'en' + }) + } + default: { + return Promise.reject(new Error('unhandled request')) + } + } + } + }) +} diff --git a/build/windows/pages/new-tab/locales/en/newtab.json b/build/windows/pages/new-tab/locales/en/newtab.json new file mode 100644 index 000000000..6d61912fc --- /dev/null +++ b/build/windows/pages/new-tab/locales/en/newtab.json @@ -0,0 +1,16 @@ +{ + "smartling": { + "string_format": "icu", + "translate_paths": [ + { + "path": "*/title", + "key": "{*}/title", + "instruction": "*/note" + } + ] + }, + "helloWorld": { + "title": "Hello world!", + "note": "here!" + } +} diff --git a/build/windows/pages/onboarding/js/index.js b/build/windows/pages/onboarding/js/index.js index d77743314..998401264 100644 --- a/build/windows/pages/onboarding/js/index.js +++ b/build/windows/pages/onboarding/js/index.js @@ -8440,7 +8440,7 @@ } catch (e3) { console.error("could not access handlers for %s, falling back to mock interface", opts.injectName); } - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/packages/special-pages/index.mjs b/packages/special-pages/index.mjs index aaeaf484d..960c3cddd 100644 --- a/packages/special-pages/index.mjs +++ b/packages/special-pages/index.mjs @@ -56,6 +56,11 @@ export const support = { 'integration': ['copy', 'build-js'], 'apple': ['copy', 'build-js', 'inline-html'], }, + /** @type {Partial>} */ + 'new-tab': { + 'integration': ['copy', 'build-js'], + 'windows': ['copy', 'build-js'], + }, } /** @type {{src: string, dest: string, injectName: string}[]} */ @@ -157,7 +162,8 @@ for (const buildJob of buildJobs) { 'import.meta.env': JSON.stringify(NODE_ENV), 'import.meta.injectName': JSON.stringify(buildJob.injectName), 'import.meta.pageName': JSON.stringify(buildJob.pageName), - } + }, + dropLabels: buildJob.injectName === "integration" ? [] : ["$INTEGRATION"] }) } } diff --git a/packages/special-pages/messages/new-tab/initialSetup.request.json b/packages/special-pages/messages/new-tab/initialSetup.request.json new file mode 100644 index 000000000..0af74a319 --- /dev/null +++ b/packages/special-pages/messages/new-tab/initialSetup.request.json @@ -0,0 +1,3 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/packages/special-pages/messages/new-tab/initialSetup.response.json b/packages/special-pages/messages/new-tab/initialSetup.response.json new file mode 100644 index 000000000..2a8f3abe7 --- /dev/null +++ b/packages/special-pages/messages/new-tab/initialSetup.response.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["widgets", "widgetConfig", "locale", "env", "platform"], + "properties": { + "widgets": { + "$ref": "./types/widget-list.json" + }, + "widgetConfig": { + "$ref": "./types/widget-config.json" + }, + "locale": { + "type": "string" + }, + "env": { + "type": "string", + "enum": ["development", "production"] + }, + "platform": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "enum": ["macos", "windows", "android", "ios"] + } + } + } + } +} diff --git a/packages/special-pages/messages/new-tab/onWidgetConfigUpdated.subscribe.json b/packages/special-pages/messages/new-tab/onWidgetConfigUpdated.subscribe.json new file mode 100644 index 000000000..8d0f33369 --- /dev/null +++ b/packages/special-pages/messages/new-tab/onWidgetConfigUpdated.subscribe.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["widgetConfig"], + "properties": { + "widgetConfig": { + "$ref": "./types/widget-config.json" + } + } +} diff --git a/packages/special-pages/messages/new-tab/reportInitException.notify.json b/packages/special-pages/messages/new-tab/reportInitException.notify.json new file mode 100644 index 000000000..afd7d6bde --- /dev/null +++ b/packages/special-pages/messages/new-tab/reportInitException.notify.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } +} diff --git a/packages/special-pages/messages/new-tab/reportPageException.notify.json b/packages/special-pages/messages/new-tab/reportPageException.notify.json new file mode 100644 index 000000000..afd7d6bde --- /dev/null +++ b/packages/special-pages/messages/new-tab/reportPageException.notify.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } +} diff --git a/packages/special-pages/messages/new-tab/setWidgetConfig.notify.json b/packages/special-pages/messages/new-tab/setWidgetConfig.notify.json new file mode 100644 index 000000000..8d0f33369 --- /dev/null +++ b/packages/special-pages/messages/new-tab/setWidgetConfig.notify.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["widgetConfig"], + "properties": { + "widgetConfig": { + "$ref": "./types/widget-config.json" + } + } +} diff --git a/packages/special-pages/messages/new-tab/types/widget-config.json b/packages/special-pages/messages/new-tab/types/widget-config.json new file mode 100644 index 000000000..ca20ed012 --- /dev/null +++ b/packages/special-pages/messages/new-tab/types/widget-config.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "title": "Widget Config", + "description": "Configuration settings for widgets", + "items": { + "type": "object", + "required": [ + "id", + "visibility" + ], + "title": "Widget Config Item", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the widget." + }, + "visibility": { + "title": "Widget Visibility", + "description": "The visibility state of the widget, as configured by the user", + "type": "string", + "enum": [ + "visible", + "hidden" + ] + } + } + } +} diff --git a/packages/special-pages/messages/new-tab/types/widget-list.json b/packages/special-pages/messages/new-tab/types/widget-list.json new file mode 100644 index 000000000..2a57c5925 --- /dev/null +++ b/packages/special-pages/messages/new-tab/types/widget-list.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "title": "Widget List", + "description": "An ordered list of supported Widgets. Use this to communicate what's supported", + "items": { + "type": "object", + "required": ["id"], + "title": "Widget List Item", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the widget." + } + } + } +} diff --git a/packages/special-pages/pages/duckplayer/app/index.js b/packages/special-pages/pages/duckplayer/app/index.js index ee978acc9..f4ff62d3c 100644 --- a/packages/special-pages/pages/duckplayer/app/index.js +++ b/packages/special-pages/pages/duckplayer/app/index.js @@ -10,7 +10,7 @@ import { Settings } from './settings.js' import { SettingsProvider } from './providers/SettingsProvider.jsx' import { MessagingContext } from './types.js' import { UserValuesProvider } from './providers/UserValuesProvider.jsx' -import { Fallback } from './components/Fallback.jsx' +import { Fallback } from '../../../shared/components/Fallback/Fallback.jsx' import { Components } from './components/Components.jsx' import { MobileApp } from './components/MobileApp.jsx' import { DesktopApp } from './components/DesktopApp.jsx' diff --git a/packages/special-pages/pages/new-tab/app/components/App.js b/packages/special-pages/pages/new-tab/app/components/App.js new file mode 100644 index 000000000..55ad8ff65 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/components/App.js @@ -0,0 +1,10 @@ +import { h } from 'preact' +import styles from './App.module.css' + +export function App ({ children }) { + return ( +
+ {children} +
+ ) +} diff --git a/packages/special-pages/pages/new-tab/app/components/App.module.css b/packages/special-pages/pages/new-tab/app/components/App.module.css new file mode 100644 index 000000000..658209802 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/components/App.module.css @@ -0,0 +1,7 @@ +.layout { + padding-top: var(--sp-16); + padding-bottom: var(--sp-16); + max-width: 504px; + margin-left: auto; + margin-right: auto; +} diff --git a/packages/special-pages/pages/new-tab/app/index.js b/packages/special-pages/pages/new-tab/app/index.js new file mode 100644 index 000000000..6a86b8390 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/index.js @@ -0,0 +1,89 @@ +import { render, h } from 'preact' +import './styles/base.css' // global styles +import { App } from './components/App.js' +import { EnvironmentProvider, UpdateEnvironment } from '../../../shared/components/EnvironmentProvider.js' +import { Fallback } from '../../../shared/components/Fallback/Fallback.jsx' +import { ErrorBoundary } from '../../../shared/components/ErrorBoundary.js' +import { SettingsProvider } from './settings.provider.js' +import { MessagingContext } from './types' +import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js' +import { WidgetConfigAPI } from './widget-list/widget-config.js' +import enStrings from '../src/locales/en/newtab.json' +import { WidgetConfigProvider } from './widget-list/widget-config.provider.js' +import { WidgetList } from './widget-list/WidgetList.js' +import { Settings } from './settings.js' + +/** + * @param {import("../src/js").NewTabPage} messaging + * @param {import("../../../shared/environment").Environment} baseEnvironment + */ +export async function init (messaging, baseEnvironment) { + const init = await messaging.init() + + if (!Array.isArray(init.widgets)) { + throw new Error('missing critical initialSetup.widgets array') + } + if (!Array.isArray(init.widgetConfig)) { + throw new Error('missing critical initialSetup.widgetConfig array') + } + + // Create an instance of the global widget api + const widgetConfigAPI = new WidgetConfigAPI(messaging, init.widgetConfig) + + // update the 'env' in case it was changed by native sides + const environment = baseEnvironment + .withEnv(init.env) + .withLocale(init.locale) + .withLocale(baseEnvironment.urlParams.get('locale')) + .withTextLength(baseEnvironment.urlParams.get('textLength')) + .withDisplay(baseEnvironment.urlParams.get('display')) + + console.log('environment:', environment) + console.log('locale:', environment.locale) + + const strings = environment.locale === 'en' + ? enStrings + : await fetch(`./locales/${environment.locale}/new-tab.json`) + .then(x => x.json()) + .catch(e => { + console.error('Could not load locale', environment.locale, e) + return enStrings + }) + + const settings = new Settings({}) + .withPlatformName(baseEnvironment.injectName) + .withPlatformName(init.platform?.name) + .withPlatformName(baseEnvironment.urlParams.get('platform')) + + const didCatch = (error) => { + const message = error?.message || error?.error || 'unknown' + messaging.reportPageException({ message }) + } + + const root = document.querySelector('#app') + if (!root) throw new Error('could not render, root element missing') + + render( + + }> + + + + + + + + + + + + + + + , + root + ) +} diff --git a/packages/special-pages/pages/new-tab/app/settings.js b/packages/special-pages/pages/new-tab/app/settings.js new file mode 100644 index 000000000..42fa73fe2 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/settings.js @@ -0,0 +1,23 @@ +export class Settings { + /** + * @param {object} params + * @param {{name: ImportMeta['platform']}} [params.platform] + */ + constructor ({ + platform = { name: 'windows' } + }) { + this.platform = platform + } + + withPlatformName (name) { + /** @type {ImportMeta['platform'][]} */ + const valid = ['windows', 'macos', 'ios', 'android'] + if (valid.includes(/** @type {any} */(name))) { + return new Settings({ + ...this, + platform: { name } + }) + } + return this + } +} diff --git a/packages/special-pages/pages/new-tab/app/settings.provider.js b/packages/special-pages/pages/new-tab/app/settings.provider.js new file mode 100644 index 000000000..3f71e1eb8 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/settings.provider.js @@ -0,0 +1,16 @@ +import { h, createContext } from 'preact' + +const SettingsContext = createContext(/** @type {{settings: import("./settings.js").Settings}} */({})) + +/** + * @param {object} params + * @param {import("./settings.js").Settings} params.settings + * @param {import("preact").ComponentChild} params.children + */ +export function SettingsProvider ({ settings, children }) { + return ( + + {children} + + ) +} diff --git a/packages/special-pages/pages/new-tab/app/styles/base.css b/packages/special-pages/pages/new-tab/app/styles/base.css new file mode 100644 index 000000000..74f4d01ab --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/styles/base.css @@ -0,0 +1,45 @@ +*, *:after, *:before { + box-sizing: border-box +} +html[data-reduced-motion=true] * { + animation: none!important; + transition: none!important; +} +/* Base styles */ +body { + font-family: system-ui; + margin: 0; + + height: 100vh; + width: 100%; + overflow-x: hidden; + + /* Make it feel more like something native */ + user-select: none; + -webkit-user-select: none; + cursor: default; + color: var(--theme-txt-color); + background: var(--theme-page-bg); +} +body > main { + width: 100%; +} +h1, +h2, +h3, +h4 { + margin: 0; +} +button { + font-family: system-ui, sans-serif; +} +ul { + margin: 0; + padding: 0; +} + +li { + list-style: none; + margin: 0; + padding: 0; +} diff --git a/packages/special-pages/pages/new-tab/app/types.js b/packages/special-pages/pages/new-tab/app/types.js new file mode 100644 index 000000000..32d7e6b03 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/types.js @@ -0,0 +1,18 @@ +import { useContext } from 'preact/hooks' +import { TranslationContext } from '../../../shared/components/TranslationsProvider.js' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import json from '../src/locales/en/newtab.json' +import { createContext } from 'preact' + +/** + * This is a wrapper to only allow keys from the default translation file + * @type {() => { t: (key: keyof json, replacements?: Record) => string }} + */ +export function useTypedTranslation () { + return { + t: useContext(TranslationContext).t + } +} + +export const MessagingContext = createContext(/** @type {import("../src/js/index.js").NewTabPage} */({})) +export const useMessaging = () => useContext(MessagingContext) diff --git a/packages/special-pages/pages/new-tab/app/widget-list/WidgetList.js b/packages/special-pages/pages/new-tab/app/widget-list/WidgetList.js new file mode 100644 index 000000000..7846ab023 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/widget-list/WidgetList.js @@ -0,0 +1,56 @@ +import { h, Fragment } from 'preact' +import { useVisibility, WidgetConfigContext, WidgetVisibilityProvider } from './widget-config.provider.js' +import { useContext } from 'preact/hooks' + +const widgetMap = { + favorites: () => , + privacyStats: () => +} + +export function WidgetList () { + const { widgets, widgetConfig } = useContext(WidgetConfigContext) + + return ( +
+ {widgets.map((widget) => { + const matchingConfig = widgetConfig.find(item => item.id === widget.id) + if (!matchingConfig) { + console.warn('missing config for widget: ', widget) + return null + } + return ( + + + {widgetMap[widget.id]?.()} + + + ) + })} +
+ ) +} + +function Favorites () { + const { visibility, id, toggle } = useVisibility() + return ( +
+

Favorites Component

+ {id} visibility: {visibility}{' '} + +
+ ) +} + +function PrivacyStats () { + const { visibility, id, toggle } = useVisibility() + return ( +
+

Privacy Stats Component

+ {id} visibility: {visibility}{' '} + +
+ ) +} diff --git a/packages/special-pages/pages/new-tab/app/widget-list/widget-config.js b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.js new file mode 100644 index 000000000..330347b87 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.js @@ -0,0 +1,144 @@ +/** + * @typedef {import("../../../../types/new-tab.js").WidgetConfig} WidgetConfig + */ + +/** + * The public API. Use this to subscribe to updates + */ +export class WidgetConfigAPI { + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {WidgetConfig} initialData - Initial widget configuration. + */ + constructor (ntp, initialData) { + this.ntp = ntp + this.manager = new WidgetConfigManager(ntp, initialData) + } + + /** + * @param {(data: WidgetConfig) => void} cb + * @return {() => void} - call this returned method to dispose of the subscription + */ + onUpdate (cb) { + const controller = new AbortController() + this.manager.eventTarget.addEventListener(this.manager.DATA_CHANGE_EVT, (/** @type {CustomEvent} */evt) => { + cb(evt.detail) + }, { signal: controller.signal }) + return () => controller.abort() + } + + /** + * @param {string} id + */ + show (id) { + const next = this.manager.inMemoryData.map(w => { + if (w.id === id) return { ...w, visibility: /** @type {const} */('visible') } + return w + }) + this.manager.update(next) + } + + /** + * @param {string} id + */ + hide (id) { + const next = this.manager.inMemoryData.map(w => { + if (w.id === id) return { ...w, visibility: /** @type {const} */('hidden') } + return w + }) + this.manager.update(next) + } +} + +class WidgetConfigManager { + debounceTimer = null + eventTarget = new EventTarget() + DATA_CHANGE_EVT = 'dataChanged' + DEBOUNCE_TIME_MS = 200 + /** + * @param {import("../../src/js/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. + * @param {WidgetConfig} initialData - Initial widget configuration. + */ + constructor (ntp, initialData) { + this.ntp = ntp + this.inMemoryData = initialData + + // Set up the subscription data feed as a source + this.setupSubscriptionStream() + } + + /** + * Sets up the subscription stream from the data feed, which behaves like an input source + * that updates in-memory data but does not trigger persistence. + */ + setupSubscriptionStream () { + // this subscription lives for the lifespan of the page, so doesn't need cleanup logic + this.ntp.messaging.subscribe('onWidgetConfigUpdated', (newData) => { + this.updateInMemoryData(newData.widgetConfig, 'subscription') + }) + } + + /** + * Manually trigger an update, for example, from a UI element. + * @param {WidgetConfig} newData - The new widget configuration to update. + */ + update (newData) { + this.updateInMemoryData(newData, 'manual') + } + + /** + * Updates the in-memory data and triggers persistence if the source is a manual update. + * This method centralizes all state updates. + * @param {WidgetConfig} newData - The new widget configuration to update in memory. + * @param {'manual' | 'subscription'} source - The source of the update. Either 'subscription' or 'manual'. + */ + updateInMemoryData (newData, source) { + this.log(`Updating in-memory data from '${source}:'`, newData) + this.inMemoryData = structuredClone(newData) // Create new immutable state + this.broadcastChange() + + // If the source is 'manual', debounce the save operation + if (source === 'manual') { + this.clearDebounceTimer() + this.debounceTimer = /** @type {any} */(setTimeout(() => { + this.persist() + }, this.DEBOUNCE_TIME_MS)) + } + } + + /** + * Clears the debounce timer if it exists, simulating the switchMap behavior. + */ + clearDebounceTimer () { + if (this.debounceTimer) { + this.log('Clearing previous debounce timer.') + clearTimeout(this.debounceTimer) + this.debounceTimer = null + } + } + + /** + * Broadcasts the current state to external subscribers using EventTarget. + */ + broadcastChange () { + this.log('Broadcasting change to external listeners:', this.inMemoryData) + this.eventTarget.dispatchEvent(new CustomEvent(this.DATA_CHANGE_EVT, { detail: this.inMemoryData })) + } + + /** + * Persists the current in-memory widget configuration state to the internal data feed. + */ + persist () { + this.log('will persist data to backend:', this.inMemoryData) + this.ntp.messaging.notify('setWidgetConfig', { widgetConfig: this.inMemoryData }) + } + + /** + * Logs messages to the console for tracing internal state updates. + * @param {string} message - The message to log. + * @param {any} data - The associated data for the message. + */ + log (message, data = null) { + console.log(`[WidgetConfigManager] ${message}`, data) + } +} diff --git a/packages/special-pages/pages/new-tab/app/widget-list/widget-config.provider.js b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.provider.js new file mode 100644 index 000000000..24b132115 --- /dev/null +++ b/packages/special-pages/pages/new-tab/app/widget-list/widget-config.provider.js @@ -0,0 +1,115 @@ +import { createContext, h } from 'preact' +import { useCallback, useContext, useEffect, useState } from 'preact/hooks' + +/** + * @typedef {import('../../../../types/new-tab.js').WidgetConfig} WidgetConfig + * @typedef {import('../../../../types/new-tab.js').WidgetList} WidgetList + * @typedef {import("../../../../types/new-tab.js").WidgetConfigItem} WidgetConfigItem + * @typedef {import("./widget-config.js").WidgetConfigAPI} WidgetConfigAPI + */ + +export const WidgetConfigContext = createContext({ + /** @type {WidgetList} */ + widgets: [], + + /** @type {WidgetConfig} */ + widgetConfig: [], + + /** @type {(name:string) => void} */ + hide: () => { + + }, + + /** @type {(name:string) => void} */ + show: () => { + + } +}) + +export const WidgetConfigDispatchContext = createContext({ + dispatch: null +}) + +/** + * @param {object} props + * @param {import("preact").ComponentChild} props.children + * @param {WidgetConfig} props.widgetConfig - the initial config data + * @param {WidgetList} props.widgets - the initial widget list + * @param {WidgetConfigAPI} props.api - the stateful API manager + */ +export function WidgetConfigProvider (props) { + const [data, setData] = useState(props.widgetConfig) + + useEffect(() => { + const unsub = props.api.onUpdate((widgetConfig) => { + setData(widgetConfig) + }) + return () => unsub() + }, [props.api]) + + /** + * @param {string} name + */ + function hide (name) { + console.log('will hide', name) + props.api.hide(name) + } + + /** + * @param {string} name + */ + function show (name) { + console.log('will show', name) + props.api.show(name) + } + + return ( + + {props.children} + + ) +} + +const WidgetVisibilityContext = createContext({ + visibility: /** @type {WidgetConfigItem['visibility']} */('visible'), + id: /** @type {WidgetConfigItem['id']} */(''), + toggle: () => { + + } +}) + +export function useVisibility () { + return useContext(WidgetVisibilityContext) +} + +/** + * @param {object} props + * @param {WidgetConfigItem['id']} props.id - the current id key used for storage + * @param {WidgetConfigItem['visibility']} props.visibility - the current id key used for storage + * @param {import("preact").ComponentChild} props.children + */ +export function WidgetVisibilityProvider (props) { + const { widgetConfig, show, hide } = useContext(WidgetConfigContext) + + const toggle = useCallback(() => { + const matching = widgetConfig.find(x => x.id === props.id) + if (matching?.visibility === 'visible') { + hide(props.id) + } else { + show(props.id) + } + }, [props.id, widgetConfig]) + + return {props.children} +} diff --git a/packages/special-pages/pages/new-tab/src/index.html b/packages/special-pages/pages/new-tab/src/index.html new file mode 100644 index 000000000..6d0102db4 --- /dev/null +++ b/packages/special-pages/pages/new-tab/src/index.html @@ -0,0 +1,14 @@ + + + + New Tab Page + + + + + + +
+ + + diff --git a/packages/special-pages/pages/new-tab/src/js/index.js b/packages/special-pages/pages/new-tab/src/js/index.js new file mode 100644 index 000000000..55c80a776 --- /dev/null +++ b/packages/special-pages/pages/new-tab/src/js/index.js @@ -0,0 +1,79 @@ +import 'preact/devtools' +/** + * @module New Tab Page + * @category Special Pages + * + * New Tab Page + * + */ +import { init } from '../../app/index.js' +import { + createTypedMessages +} from '@duckduckgo/messaging' +import { createSpecialPageMessaging } from '../../../../shared/create-special-page-messaging' +import { Environment } from '../../../../shared/environment.js' +import { mockTransport } from './mock-transport.js' + +export class NewTabPage { + /** + * @param {import("@duckduckgo/messaging").Messaging} messaging + * @param {ImportMeta['injectName']} injectName + */ + constructor (messaging, injectName) { + /** + * @internal + */ + this.messaging = createTypedMessages(this, messaging) + this.injectName = injectName + } + + /** + * @return {Promise} + */ + init () { + return this.messaging.request('initialSetup') + } + + /** + * @param {string} message + */ + reportInitException (message) { + this.messaging.notify('reportInitException', { message }) + } + + /** + * This will be sent if the application has loaded, but a client-side error + * has occurred that cannot be recovered from + * @param {{message: string}} params + */ + reportPageException (params) { + this.messaging.notify('reportPageException', params) + } +} + +const baseEnvironment = new Environment() + .withInjectName(import.meta.injectName) + .withEnv(import.meta.env) + +const messaging = createSpecialPageMessaging({ + injectName: import.meta.injectName, + env: import.meta.env, + pageName: 'newTabPage', + mockTransport: () => { + // only in integration environments + if (baseEnvironment.injectName !== 'integration') return null + // never in playwright environments + if (window.__playwright_01) return null + let mock = null + // eslint-disable-next-line no-labels + $INTEGRATION: mock = mockTransport() + return mock + } +}) + +const newTabMessaging = new NewTabPage(messaging, import.meta.injectName) +init(newTabMessaging, baseEnvironment).catch(e => { + console.error(e) + const msg = typeof e?.message === 'string' ? e.message : 'unknown init error' + newTabMessaging.reportInitException(msg) +}) diff --git a/packages/special-pages/pages/new-tab/src/js/inline.js b/packages/special-pages/pages/new-tab/src/js/inline.js new file mode 100644 index 000000000..6d912bfc5 --- /dev/null +++ b/packages/special-pages/pages/new-tab/src/js/inline.js @@ -0,0 +1 @@ +// remove diff --git a/packages/special-pages/pages/new-tab/src/js/mock-transport.js b/packages/special-pages/pages/new-tab/src/js/mock-transport.js new file mode 100644 index 000000000..ca6b115d2 --- /dev/null +++ b/packages/special-pages/pages/new-tab/src/js/mock-transport.js @@ -0,0 +1,103 @@ +import { TestTransportConfig } from '@duckduckgo/messaging' + +export function mockTransport () { + const channel = new BroadcastChannel('ntp') + + function broadcast () { + setTimeout(() => { + channel.postMessage({ + change: 'ntp.widgetConfig' + }) + }, 100) + } + + /** + * @param {string} name + * @return {Record|null} + */ + function read (name) { + try { + const item = localStorage.getItem(name) + if (!item) return null + console.log('did read from LS', item) + return JSON.parse(item) + } catch (e) { + console.error('Failed to parse initialSetup from localStorage', e) + return null + } + } + + /** + * @param {string} name + * @param {Record} value + */ + function write (name, value) { + try { + localStorage.setItem(name, JSON.stringify(value)) + console.log('✅ did write') + } catch (e) { + console.error('Failed to write', e) + } + } + + return new TestTransportConfig({ + notify (msg) { + switch (msg.method) { + case 'setWidgetConfig': { + if (!msg.params) throw new Error('unreachable') + write('ntp.widgetConfig', msg.params) + broadcast() + return + } + default: { + console.warn('unhandled notification', msg) + } + } + }, + subscribe (sub, cb) { + switch (sub.subscriptionName) { + case 'onWidgetConfigUpdated': { + const controller = new AbortController() + // console.log('sub?', sub, cb); + channel.addEventListener('message', () => { + const values = read('ntp.widgetConfig') + if (values) { + cb(values) + } + }, { signal: controller.signal }) + return () => controller.abort() + } + } + return () => {} + }, + // eslint-ignore-next-line require-await + request (msg) { + switch (msg.method) { + case 'initialSetup': { + const widgetsFromStorage = read('ntp.widgets') || { + widgets: [ + { id: 'favorites' }, + { id: 'privacyStats' } + ] + } + const widgetConfigFromStorage = read('ntp.widgetConfig') || { + widgetConfig: [ + { id: 'favorites', visibility: 'visible' }, + { id: 'privacyStats', visibility: 'visible' } + ] + } + return Promise.resolve({ + widgets: widgetsFromStorage.widgets, + widgetConfig: widgetConfigFromStorage.widgetConfig, + platform: { name: 'integration' }, + env: 'development', + locale: 'en' + }) + } + default: { + return Promise.reject(new Error('unhandled request')) + } + } + } + }) +} diff --git a/packages/special-pages/pages/new-tab/src/locales/en/newtab.json b/packages/special-pages/pages/new-tab/src/locales/en/newtab.json new file mode 100644 index 000000000..6d61912fc --- /dev/null +++ b/packages/special-pages/pages/new-tab/src/locales/en/newtab.json @@ -0,0 +1,16 @@ +{ + "smartling": { + "string_format": "icu", + "translate_paths": [ + { + "path": "*/title", + "key": "{*}/title", + "instruction": "*/note" + } + ] + }, + "helloWorld": { + "title": "Hello world!", + "note": "here!" + } +} diff --git a/packages/special-pages/playwright.config.js b/packages/special-pages/playwright.config.js index 4b10c9c9d..9b3fcb6c5 100644 --- a/packages/special-pages/playwright.config.js +++ b/packages/special-pages/playwright.config.js @@ -7,7 +7,8 @@ export default defineConfig({ testMatch: [ 'duckplayer.spec.js', 'duckplayer-screenshots.spec.js', - 'onboarding.spec.js' + 'onboarding.spec.js', + 'new-tab.spec.js' ], use: { ...devices['Desktop Edge'], diff --git a/packages/special-pages/shared/components/Fallback/Fallback.jsx b/packages/special-pages/shared/components/Fallback/Fallback.jsx new file mode 100644 index 000000000..5555dbdcc --- /dev/null +++ b/packages/special-pages/shared/components/Fallback/Fallback.jsx @@ -0,0 +1,19 @@ +import { h } from "preact"; +import styles from "./Fallback.module.css"; + +/** + * @param {object} props + * @param {boolean} props.showDetails + */ +export function Fallback({showDetails}) { + return ( +
+
+

Something went wrong!

+ {showDetails && ( +

Please check logs for a message called reportPageException

+ )} +
+
+ ) +} diff --git a/packages/special-pages/shared/components/Fallback/Fallback.module.css b/packages/special-pages/shared/components/Fallback/Fallback.module.css new file mode 100644 index 000000000..6e689c17f --- /dev/null +++ b/packages/special-pages/shared/components/Fallback/Fallback.module.css @@ -0,0 +1,4 @@ +.fallback { + height: 100%; + width: 100%; +} diff --git a/packages/special-pages/shared/create-special-page-messaging.js b/packages/special-pages/shared/create-special-page-messaging.js index 9cdf24d37..7ad91a695 100644 --- a/packages/special-pages/shared/create-special-page-messaging.js +++ b/packages/special-pages/shared/create-special-page-messaging.js @@ -12,6 +12,7 @@ import { * @param {ImportMeta['env']} opts.env * @param {ImportMeta['injectName']} opts.injectName * @param {string} opts.pageName + * @param {(() => TestTransportConfig|null) | null | undefined} [opts.mockTransport] * @internal */ export function createSpecialPageMessaging (opts) { @@ -55,7 +56,7 @@ export function createSpecialPageMessaging (opts) { } // this fallback allows for the 'integration' target to run without errors - const fallback = new TestTransportConfig({ + const fallback = opts.mockTransport?.() || new TestTransportConfig({ /** * @param {import('@duckduckgo/messaging').NotificationMessage} msg */ diff --git a/packages/special-pages/tests/new-tab.spec.js b/packages/special-pages/tests/new-tab.spec.js new file mode 100644 index 000000000..3ab426a71 --- /dev/null +++ b/packages/special-pages/tests/new-tab.spec.js @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test' +import { NewtabPage } from './page-objects/newtab' + +test.describe('newtab widgets', () => { + test('widget config single click', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo) + await ntp.reducedMotion() + await ntp.openPage() + + // hide + await page.getByRole('button', { name: 'Toggle Privacy Stats' }).click() + await expect(page.locator('#app')).toContainText('privacyStats visibility: hidden') + + // debounced + await page.waitForTimeout(500) + + // verify the single sync call, where one is hidden + const outgoing = await ntp.mocks.outgoing({ names: ['setWidgetConfig'] }) + expect(outgoing).toStrictEqual([{ + payload: { + context: 'specialPages', + featureName: 'newTabPage', + params: { + widgetConfig: [ + { id: 'favorites', visibility: 'visible' }, + { id: 'privacyStats', visibility: 'hidden' } + ] + }, + method: 'setWidgetConfig' + } + }]) + }) + test('widget config double click', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo) + await ntp.reducedMotion() + await ntp.openPage() + + // hide + await page.getByRole('button', { name: 'Toggle Privacy Stats' }).click() + await expect(page.locator('#app')).toContainText('privacyStats visibility: hidden') + + // show + await page.getByRole('button', { name: 'Toggle Privacy Stats' }).click() + await expect(page.locator('#app')).toContainText('privacyStats visibility: visible') + + // debounced + await page.waitForTimeout(500) + + // verify the single sync call, where both are visible. + const outgoing = await ntp.mocks.outgoing({ names: ['setWidgetConfig'] }) + expect(outgoing).toStrictEqual([{ + payload: { + context: 'specialPages', + featureName: 'newTabPage', + params: { + widgetConfig: [ + { id: 'favorites', visibility: 'visible' }, + { id: 'privacyStats', visibility: 'visible' } + ] + }, + method: 'setWidgetConfig' + } + }]) + }) +}) diff --git a/packages/special-pages/tests/page-objects/newtab.js b/packages/special-pages/tests/page-objects/newtab.js new file mode 100644 index 000000000..93134560b --- /dev/null +++ b/packages/special-pages/tests/page-objects/newtab.js @@ -0,0 +1,103 @@ +import { Mocks } from './mocks.js' +import { perPlatform } from '../../../../integration-test/playwright/type-helpers.mjs' +import { join } from 'node:path' + +/** + * @typedef {import('../../../../integration-test/playwright/type-helpers.mjs').Build} Build + * @typedef {import('../../../../integration-test/playwright/type-helpers.mjs').PlatformInfo} PlatformInfo + */ + +export class NewtabPage { + /** + * @param {import("@playwright/test").Page} page + * @param {Build} build + * @param {PlatformInfo} platform + */ + constructor (page, build, platform) { + this.page = page + this.build = build + this.platform = platform + this.mocks = new Mocks(page, build, platform, { + context: 'specialPages', + featureName: 'newTabPage', + env: 'development' + }) + this.page.on('console', console.log) + // default mocks - just enough to render the first page without error + this.mocks.defaultResponses({ + requestSetAsDefault: {}, + requestImport: {}, + /** @type {import('../../types/new-tab.js').InitialSetupResponse} */ + initialSetup: { + widgets: [ + { id: 'favorites' }, + { id: 'privacyStats' } + ], + widgetConfig: [ + { id: 'favorites', visibility: 'visible' }, + { id: 'privacyStats', visibility: 'visible' } + ], + env: 'development', + locale: 'en', + platform: { + name: this.platform.name || 'windows' + } + } + }) + } + + /** + * Opens a page with optional parameters. + * This method ensures that mocks are installed and routes are set up before navigating to the page. + * + * @param {Object} [params] - Optional parameters for opening the page. + * @param {'debug' | 'production'} [params.mode] - Optional parameters for opening the page. + * @param {boolean} [params.willThrow] - Optional flag to simulate an exception + */ + async openPage ({ mode = 'debug', willThrow = false } = { }) { + await this.mocks.install() + await this.page.route('/**', (route, req) => { + const url = new URL(req.url()) + // try to serve assets, but change `/` to 'index' + let filepath = url.pathname + if (filepath === '/') filepath = 'index.html' + + return route.fulfill({ + status: 200, + path: join(this.basePath, filepath) + }) + }) + const searchParams = new URLSearchParams({ mode, willThrow: String(willThrow) }) + await this.page.goto('/' + '?' + searchParams.toString()) + } + + /** + * We test the fully built artifacts, so for each test run we need to + * select the correct HTML file. + * @return {string} + */ + get basePath () { + return this.build.switch({ + windows: () => '../../build/windows/pages/new-tab', + integration: () => '../../build/integration/pages/new-tab' + }) + } + + /** + * @param {import("@playwright/test").Page} page + * @param {import("@playwright/test").TestInfo} testInfo + */ + static create (page, testInfo) { + // Read the configuration object to determine which platform we're testing against + const { platformInfo, build } = perPlatform(testInfo.project.use) + return new NewtabPage(page, build, platformInfo) + } + + async reducedMotion () { + await this.page.emulateMedia({ reducedMotion: 'reduce' }) + } + + async darkMode () { + await this.page.emulateMedia({ colorScheme: 'dark' }) + } +} diff --git a/packages/special-pages/types/new-tab.ts b/packages/special-pages/types/new-tab.ts new file mode 100644 index 000000000..48d376a3d --- /dev/null +++ b/packages/special-pages/types/new-tab.ts @@ -0,0 +1,107 @@ +/** + * @module NewTab Messages + * @description + * + * These types are auto-generated from schema files. + * scripts/build-types.mjs is responsible for type generation. + * **DO NOT** edit this file directly as your changes will be lost. + */ + +/** + * The visibility state of the widget, as configured by the user + */ +export type WidgetVisibility = "visible" | "hidden"; +/** + * Configuration settings for widgets + */ +export type WidgetConfig = WidgetConfigItem[]; +/** + * An ordered list of supported Widgets. Use this to communicate what's supported + */ +export type WidgetList = WidgetListItem[]; + +/** + * Requests, Notifications and Subscriptions from the NewTab feature + */ +export interface NewTabMessages { + notifications: ReportInitExceptionNotification | ReportPageExceptionNotification | SetWidgetConfigNotification; + requests: InitialSetupRequest; + subscriptions: OnWidgetConfigUpdatedSubscription; +} +/** + * Generated from @see "../messages/new-tab/reportInitException.notify.json" + */ +export interface ReportInitExceptionNotification { + method: "reportInitException"; + params: ReportInitExceptionNotify; +} +export interface ReportInitExceptionNotify { + message: string; +} +/** + * Generated from @see "../messages/new-tab/reportPageException.notify.json" + */ +export interface ReportPageExceptionNotification { + method: "reportPageException"; + params: ReportPageExceptionNotify; +} +export interface ReportPageExceptionNotify { + message: string; +} +/** + * Generated from @see "../messages/new-tab/setWidgetConfig.notify.json" + */ +export interface SetWidgetConfigNotification { + method: "setWidgetConfig"; + params: SetWidgetConfigNotify; +} +export interface SetWidgetConfigNotify { + widgetConfig: WidgetConfig; +} +export interface WidgetConfigItem { + /** + * A unique identifier for the widget. + */ + id: string; + visibility: WidgetVisibility; +} +/** + * Generated from @see "../messages/new-tab/initialSetup.request.json" + */ +export interface InitialSetupRequest { + method: "initialSetup"; + result: InitialSetupResponse; +} +export interface InitialSetupResponse { + widgets: WidgetList; + widgetConfig: WidgetConfig; + locale: string; + env: "development" | "production"; + platform: { + name: "macos" | "windows" | "android" | "ios"; + }; +} +export interface WidgetListItem { + /** + * A unique identifier for the widget. + */ + id: string; +} +/** + * Generated from @see "../messages/new-tab/onWidgetConfigUpdated.subscribe.json" + */ +export interface OnWidgetConfigUpdatedSubscription { + subscriptionEvent: "onWidgetConfigUpdated"; + params: OnWidgetConfigUpdatedSubscribe; +} +export interface OnWidgetConfigUpdatedSubscribe { + widgetConfig: WidgetConfig; +} + +declare module "../pages/new-tab/src/js/index.js" { + export interface NewTabPage { + notify: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['notify'], + request: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['request'], + subscribe: import("@duckduckgo/messaging/lib/shared-types").MessagingBase['subscribe'] + } +} \ No newline at end of file