@mideind/netskrafl-react 1.0.0-beta.9 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -3,6 +3,98 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var React = require('react');
5
5
 
6
+ // Key for storing auth settings in sessionStorage
7
+ const AUTH_SETTINGS_KEY = "netskrafl_auth_settings";
8
+ // Save authentication settings to sessionStorage
9
+ const saveAuthSettings = (settings) => {
10
+ if (!settings) {
11
+ clearAuthSettings();
12
+ return;
13
+ }
14
+ try {
15
+ // Filter to only include properties defined in PersistedAuthSettings interface
16
+ const filteredSettings = {
17
+ userEmail: settings.userEmail, // Required field
18
+ };
19
+ // Only add optional fields if they are defined
20
+ if (settings.userId !== undefined)
21
+ filteredSettings.userId = settings.userId;
22
+ if (settings.userNick !== undefined)
23
+ filteredSettings.userNick = settings.userNick;
24
+ if (settings.firebaseAPIKey !== undefined)
25
+ filteredSettings.firebaseAPIKey = settings.firebaseAPIKey;
26
+ if (settings.beginner !== undefined)
27
+ filteredSettings.beginner = settings.beginner;
28
+ if (settings.fairPlay !== undefined)
29
+ filteredSettings.fairPlay = settings.fairPlay;
30
+ if (settings.ready !== undefined)
31
+ filteredSettings.ready = settings.ready;
32
+ if (settings.readyTimed !== undefined)
33
+ filteredSettings.readyTimed = settings.readyTimed;
34
+ // Only save if we have actual settings to persist
35
+ if (Object.keys(filteredSettings).length > 1) {
36
+ sessionStorage.setItem(AUTH_SETTINGS_KEY, JSON.stringify(filteredSettings));
37
+ }
38
+ else {
39
+ clearAuthSettings();
40
+ }
41
+ }
42
+ catch (error) {
43
+ // SessionStorage might be unavailable or full
44
+ console.warn("Could not save auth settings to sessionStorage:", error);
45
+ }
46
+ };
47
+ // Retrieve authentication settings from sessionStorage
48
+ const loadAuthSettings = () => {
49
+ try {
50
+ const stored = sessionStorage.getItem(AUTH_SETTINGS_KEY);
51
+ if (stored) {
52
+ return JSON.parse(stored);
53
+ }
54
+ }
55
+ catch (error) {
56
+ // SessionStorage might be unavailable or data might be corrupted
57
+ console.warn("Could not load auth settings from sessionStorage:", error);
58
+ }
59
+ return null;
60
+ };
61
+ // Clear authentication settings from sessionStorage
62
+ const clearAuthSettings = () => {
63
+ try {
64
+ sessionStorage.removeItem(AUTH_SETTINGS_KEY);
65
+ }
66
+ catch (error) {
67
+ console.warn("Could not clear auth settings from sessionStorage:", error);
68
+ }
69
+ };
70
+ // Apply persisted settings to a GlobalState object
71
+ const applyPersistedSettings = (state) => {
72
+ var _a, _b, _c, _d;
73
+ const persisted = loadAuthSettings();
74
+ if (!persisted) {
75
+ return state;
76
+ }
77
+ // CRITICAL SECURITY CHECK: Only apply persisted settings if they belong to the current user
78
+ // This prevents data leakage between different users in the same browser session
79
+ if (persisted.userEmail !== state.userEmail) {
80
+ // Different user detected - clear the old user's settings
81
+ clearAuthSettings();
82
+ return state;
83
+ }
84
+ // Apply persisted settings, but don't override values explicitly passed in props
85
+ return {
86
+ ...state,
87
+ // Only apply persisted values if current values are defaults
88
+ userId: state.userId || persisted.userId || state.userId,
89
+ userNick: state.userNick || persisted.userNick || state.userNick,
90
+ firebaseAPIKey: state.firebaseAPIKey || persisted.firebaseAPIKey || state.firebaseAPIKey,
91
+ beginner: (_a = persisted.beginner) !== null && _a !== void 0 ? _a : state.beginner,
92
+ fairPlay: (_b = persisted.fairPlay) !== null && _b !== void 0 ? _b : state.fairPlay,
93
+ ready: (_c = persisted.ready) !== null && _c !== void 0 ? _c : state.ready,
94
+ readyTimed: (_d = persisted.readyTimed) !== null && _d !== void 0 ? _d : state.readyTimed,
95
+ };
96
+ };
97
+
6
98
  const DEFAULT_STATE = {
7
99
  projectId: "netskrafl",
8
100
  firebaseAPIKey: "",
@@ -23,16 +115,39 @@ const DEFAULT_STATE = {
23
115
  loginUrl: "",
24
116
  loginMethod: "",
25
117
  newUser: false,
26
- beginner: false,
27
- fairPlay: true,
118
+ beginner: true,
119
+ fairPlay: false,
28
120
  plan: "", // Not a friend
29
121
  hasPaid: false,
30
- ready: false,
31
- readyTimed: false,
122
+ ready: true,
123
+ readyTimed: true,
32
124
  uiFullscreen: true,
33
125
  uiLandscape: false,
34
126
  runningLocal: false,
35
127
  };
128
+ const makeServerUrls = (backendUrl, movesUrl) => {
129
+ // If the last character of the url is a slash, cut it off,
130
+ // since path URLs always start with a slash
131
+ const cleanupUrl = (url) => {
132
+ if (url.length > 0 && url[url.length - 1] === "/") {
133
+ url = url.slice(0, -1);
134
+ }
135
+ return url;
136
+ };
137
+ return {
138
+ serverUrl: cleanupUrl(backendUrl),
139
+ movesUrl: cleanupUrl(movesUrl),
140
+ };
141
+ };
142
+ const makeGlobalState = (overrides) => {
143
+ const state = {
144
+ ...DEFAULT_STATE,
145
+ ...overrides,
146
+ };
147
+ const stateWithUrls = { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
148
+ // Apply any persisted authentication settings from sessionStorage
149
+ return applyPersistedSettings(stateWithUrls);
150
+ };
36
151
 
37
152
  function getDefaultExportFromCjs (x) {
38
153
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -2189,53 +2304,485 @@ function requireMithril () {
2189
2304
  var mithrilExports = requireMithril();
2190
2305
  var m = /*@__PURE__*/getDefaultExportFromCjs(mithrilExports);
2191
2306
 
2192
- let BACKEND_SERVER_PREFIX = "http://127.0.0.1:3000"; // Default for development
2193
- let MOVES_SERVER_PREFIX = "https://moves-dot-explo-dev.appspot.com"; // Default for development
2194
- let MOVES_ACCESS_KEY = "None";
2195
- const setServerUrl = (backendUrl, movesUrl, movesAccessKey) => {
2196
- // If the last character of the url is a slash, cut it off,
2197
- // since path URLs always start with a slash
2198
- const cleanupUrl = (url) => {
2199
- if (url.length > 0 && url[url.length - 1] === "/") {
2200
- url = url.slice(0, -1);
2201
- }
2202
- return url;
2203
- };
2204
- if (backendUrl)
2205
- BACKEND_SERVER_PREFIX = cleanupUrl(backendUrl);
2206
- if (movesUrl)
2207
- MOVES_SERVER_PREFIX = cleanupUrl(movesUrl);
2208
- if (movesAccessKey)
2209
- MOVES_ACCESS_KEY = movesAccessKey;
2210
- };
2211
- const serverUrl = (path) => {
2212
- return `${BACKEND_SERVER_PREFIX}${path}`;
2307
+ const serverUrl = (state, path) => {
2308
+ return `${state.serverUrl}${path}`;
2213
2309
  };
2214
- const request = (options) => {
2215
- // Call the default service on the Google App Engine backend
2310
+ // Export a non-authenticated request for use by login.ts to avoid circular dependency
2311
+ const requestWithoutAuth = (state, options) => {
2216
2312
  return m.request({
2217
2313
  withCredentials: true,
2218
2314
  ...options,
2219
- url: serverUrl(options.url),
2315
+ url: serverUrl(state, options.url),
2220
2316
  });
2221
2317
  };
2222
- const requestMoves = (options) => {
2223
- // Call the moves service on the Google App Engine backend
2224
- const url = `${MOVES_SERVER_PREFIX}${options.url}`;
2225
- const headers = {
2226
- "Content-Type": "application/json; charset=UTF-8",
2227
- Authorization: `Bearer ${MOVES_ACCESS_KEY}`,
2228
- ...options === null || options === void 0 ? void 0 : options.headers,
2229
- };
2230
- return m.request({
2231
- withCredentials: false,
2232
- method: "POST",
2233
- ...options,
2234
- url,
2235
- headers,
2236
- });
2318
+
2319
+ /*
2320
+
2321
+ i8n.ts
2322
+
2323
+ Single page UI for Netskrafl/Explo using the Mithril library
2324
+
2325
+ Copyright (C) 2025 Miðeind ehf.
2326
+ Author: Vilhjálmur Þorsteinsson
2327
+
2328
+ The Creative Commons Attribution-NonCommercial 4.0
2329
+ International Public License (CC-BY-NC 4.0) applies to this software.
2330
+ For further information, see https://github.com/mideind/Netskrafl
2331
+
2332
+
2333
+ This module contains internationalization (i18n) utility functions,
2334
+ allowing for translation of displayed text between languages.
2335
+
2336
+ Text messages for individual locales are loaded from the
2337
+ /static/assets/messages.json file, which is fetched from the server.
2338
+
2339
+ */
2340
+ // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
2341
+ // This is overwritten in setLocale()
2342
+ let currentLocale = "is_IS";
2343
+ let currentFallback = "is";
2344
+ // Regex that matches embedded interpolations such as "Welcome, {username}!"
2345
+ // Interpolation identifiers should only contain ASCII characters, digits and '_'
2346
+ const rex = /{\s*(\w+)\s*}/g;
2347
+ let messages = {};
2348
+ let messagesLoaded = false;
2349
+ function hasAnyTranslation(msgs, locale) {
2350
+ // Return true if any translation is available for the given locale
2351
+ for (let key in msgs) {
2352
+ if (msgs[key][locale] !== undefined)
2353
+ return true;
2354
+ }
2355
+ return false;
2356
+ }
2357
+ function setLocale(locale, msgs) {
2358
+ // Set the current i18n locale and fallback
2359
+ currentLocale = locale;
2360
+ currentFallback = locale.split("_")[0];
2361
+ // For unsupported locales, i.e. locales that have no
2362
+ // translations available for them, fall back to English (U.S.).
2363
+ if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
2364
+ currentLocale = "en_US";
2365
+ currentFallback = "en";
2366
+ }
2367
+ // Flatten the Messages structure, enabling long strings
2368
+ // to be represented as string arrays in the messages.json file
2369
+ messages = {};
2370
+ for (let key in msgs) {
2371
+ for (let lc in msgs[key]) {
2372
+ let s = msgs[key][lc];
2373
+ if (Array.isArray(s))
2374
+ s = s.join("");
2375
+ if (messages[key] === undefined)
2376
+ messages[key] = {};
2377
+ // If the string s contains HTML markup of the form <tag>...</tag>,
2378
+ // convert it into a list of Mithril Vnode children corresponding to
2379
+ // the text and the tags
2380
+ if (s.match(/<[a-z]+>/)) {
2381
+ // Looks like the string contains HTML markup
2382
+ const vnodes = [];
2383
+ let i = 0;
2384
+ let tagMatch = null;
2385
+ while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
2386
+ // Found what looks like an HTML tag
2387
+ // Calculate the index of the enclosed text within s
2388
+ const tag = tagMatch[0];
2389
+ let j = i + tagMatch.index + tag.length;
2390
+ // Find the end tag
2391
+ let end = s.indexOf("</" + tag.slice(1), j);
2392
+ if (end < 0) {
2393
+ // No end tag - skip past this weirdness
2394
+ i = j;
2395
+ continue;
2396
+ }
2397
+ // Add the text preceding the tag
2398
+ if (tagMatch.index > 0)
2399
+ vnodes.push(s.slice(i, i + tagMatch.index));
2400
+ // Create the Mithril node corresponding to the tag and the enclosed text
2401
+ // and add it to the list
2402
+ vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
2403
+ // Advance the index past the end of the tag
2404
+ i = end + tag.length + 1;
2405
+ }
2406
+ // Push the final text part, if any
2407
+ if (i < s.length)
2408
+ vnodes.push(s.slice(i));
2409
+ // Reassign s to the list of vnodes
2410
+ s = vnodes;
2411
+ }
2412
+ messages[key][lc] = s;
2413
+ }
2414
+ }
2415
+ messagesLoaded = true;
2416
+ }
2417
+ async function loadMessages(state, locale) {
2418
+ // Load the internationalization message JSON file from the server
2419
+ // and set the user's locale
2420
+ try {
2421
+ const messages = await requestWithoutAuth(state, {
2422
+ method: "GET",
2423
+ url: "/static/assets/messages.json",
2424
+ withCredentials: false, // Cookies are not allowed for CORS request
2425
+ });
2426
+ setLocale(locale, messages);
2427
+ }
2428
+ catch (_a) {
2429
+ setLocale(locale, {});
2430
+ }
2431
+ }
2432
+ function t(key, ips = {}) {
2433
+ // Main text translation function, supporting interpolation
2434
+ // and HTML tag substitution
2435
+ const msgDict = messages[key];
2436
+ if (msgDict === undefined)
2437
+ // No dictionary for this key - may actually be a missing entry
2438
+ return messagesLoaded ? key : "";
2439
+ // Lookup exact locale, then fallback, then resort to returning the key
2440
+ const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
2441
+ // If we have an interpolation object, do the interpolation first
2442
+ return Object.keys(ips).length ? interpolate(message, ips) : message;
2443
+ }
2444
+ function ts(key, ips = {}) {
2445
+ // String translation function, supporting interpolation
2446
+ // but not HTML tag substitution
2447
+ const msgDict = messages[key];
2448
+ if (msgDict === undefined)
2449
+ // No dictionary for this key - may actually be a missing entry
2450
+ return messagesLoaded ? key : "";
2451
+ // Lookup exact locale, then fallback, then resort to returning the key
2452
+ const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
2453
+ if (typeof message != "string")
2454
+ // This is actually an error - the client should be calling t() instead
2455
+ return "";
2456
+ // If we have an interpolation object, do the interpolation first
2457
+ return Object.keys(ips).length ? interpolate_string(message, ips) : message;
2458
+ }
2459
+ function mt(cls, children) {
2460
+ // Wrapper for the Mithril m() function that auto-translates
2461
+ // string and array arguments
2462
+ if (typeof children == "string") {
2463
+ return m(cls, t(children));
2464
+ }
2465
+ if (Array.isArray(children)) {
2466
+ return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
2467
+ }
2468
+ return m(cls, children);
2469
+ }
2470
+ function interpolate(message, ips) {
2471
+ // Replace interpolation placeholders with their corresponding values
2472
+ if (typeof message == "string") {
2473
+ return message.replace(rex, (match, key) => ips[key] || match);
2474
+ }
2475
+ if (Array.isArray(message)) {
2476
+ return message.map((item) => interpolate(item, ips));
2477
+ }
2478
+ return message;
2479
+ }
2480
+ function interpolate_string(message, ips) {
2481
+ // Replace interpolation placeholders with their corresponding values
2482
+ return message.replace(rex, (match, key) => ips[key] || match);
2483
+ }
2484
+
2485
+ /*
2486
+
2487
+ Types.ts
2488
+
2489
+ Common type definitions for the Explo/Netskrafl user interface
2490
+
2491
+ Copyright (C) 2025 Miðeind ehf.
2492
+ Author: Vilhjalmur Thorsteinsson
2493
+
2494
+ The Creative Commons Attribution-NonCommercial 4.0
2495
+ International Public License (CC-BY-NC 4.0) applies to this software.
2496
+ For further information, see https://github.com/mideind/Netskrafl
2497
+
2498
+ */
2499
+ // Global constants
2500
+ const RACK_SIZE = 7;
2501
+ const ROWIDS = "ABCDEFGHIJKLMNO";
2502
+ const BOARD_SIZE = ROWIDS.length;
2503
+ const EXTRA_WIDE_LETTERS = "q";
2504
+ const WIDE_LETTERS = "zxmæ";
2505
+ const ZOOM_FACTOR = 1.5;
2506
+ const ERROR_MESSAGES = {
2507
+ // Translations are found in /static/assets/messages.json
2508
+ 1: "Enginn stafur lagður niður",
2509
+ 2: "Fyrsta orð verður að liggja um byrjunarreitinn",
2510
+ 3: "Orð verður að vera samfellt á borðinu",
2511
+ 4: "Orð verður að tengjast orði sem fyrir er",
2512
+ 5: "Reitur þegar upptekinn",
2513
+ 6: "Ekki má vera eyða í orði",
2514
+ 7: "word_not_found",
2515
+ 8: "word_not_found",
2516
+ 9: "Of margir stafir lagðir niður",
2517
+ 10: "Stafur er ekki í rekkanum",
2518
+ 11: "Of fáir stafir eftir, skipting ekki leyfð",
2519
+ 12: "Of mörgum stöfum skipt",
2520
+ 13: "Leik vantar á borðið - notið F5/Refresh",
2521
+ 14: "Notandi ekki innskráður - notið F5/Refresh",
2522
+ 15: "Rangur eða óþekktur notandi",
2523
+ 16: "Viðureign finnst ekki",
2524
+ 17: "Viðureign er ekki utan tímamarka",
2525
+ 18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
2526
+ 19: "Véfenging er ekki möguleg í þessari viðureign",
2527
+ 20: "Síðasti leikur er ekki véfengjanlegur",
2528
+ 21: "Aðeins véfenging eða pass leyfileg",
2529
+ "server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
2237
2530
  };
2238
2531
 
2532
+ /*
2533
+
2534
+ Util.ts
2535
+
2536
+ Utility functions for the Explo/Netskrafl user interface
2537
+
2538
+ Copyright (C) 2025 Miðeind ehf.
2539
+ Author: Vilhjálmur Þorsteinsson
2540
+
2541
+ The Creative Commons Attribution-NonCommercial 4.0
2542
+ International Public License (CC-BY-NC 4.0) applies to this software.
2543
+ For further information, see https://github.com/mideind/Netskrafl
2544
+
2545
+ The following code is based on
2546
+ https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
2547
+
2548
+ */
2549
+ // Global vars to cache event state
2550
+ var evCache = [];
2551
+ var origDistance = -1;
2552
+ const PINCH_THRESHOLD = 10; // Minimum pinch movement
2553
+ var hasZoomed = false;
2554
+ // Old-style (non-single-page) game URL prefix
2555
+ const BOARD_PREFIX = "/board?game=";
2556
+ const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
2557
+ function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
2558
+ // Install event handlers for the pointer target
2559
+ attrs.onpointerdown = pointerdown_handler;
2560
+ attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
2561
+ // Use same handler for pointer{up,cancel,out,leave} events since
2562
+ // the semantics for these events - in this app - are the same.
2563
+ attrs.onpointerup = pointerup_handler;
2564
+ attrs.onpointercancel = pointerup_handler;
2565
+ attrs.onpointerout = pointerup_handler;
2566
+ attrs.onpointerleave = pointerup_handler;
2567
+ }
2568
+ function pointerdown_handler(ev) {
2569
+ // The pointerdown event signals the start of a touch interaction.
2570
+ // This event is cached to support 2-finger gestures
2571
+ evCache.push(ev);
2572
+ }
2573
+ function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
2574
+ // This function implements a 2-pointer horizontal pinch/zoom gesture.
2575
+ //
2576
+ // If the distance between the two pointers has increased (zoom in),
2577
+ // the target element's background is changed to "pink" and if the
2578
+ // distance is decreasing (zoom out), the color is changed to "lightblue".
2579
+ //
2580
+ // Find this event in the cache and update its record with this event
2581
+ for (let i = 0; i < evCache.length; i++) {
2582
+ if (ev.pointerId === evCache[i].pointerId) {
2583
+ evCache[i] = ev;
2584
+ break;
2585
+ }
2586
+ }
2587
+ // If two pointers are down, check for pinch gestures
2588
+ if (evCache.length == 2) {
2589
+ // Calculate the distance between the two pointers
2590
+ const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
2591
+ Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
2592
+ if (origDistance > 0) {
2593
+ if (curDistance - origDistance >= PINCH_THRESHOLD) {
2594
+ // The distance between the two pointers has increased
2595
+ if (!hasZoomed)
2596
+ funcZoomIn();
2597
+ hasZoomed = true;
2598
+ }
2599
+ else if (origDistance - curDistance >= PINCH_THRESHOLD) {
2600
+ // The distance between the two pointers has decreased
2601
+ if (!hasZoomed)
2602
+ funcZoomOut();
2603
+ hasZoomed = true;
2604
+ }
2605
+ }
2606
+ else if (origDistance < 0) {
2607
+ // Note the original difference between two pointers
2608
+ origDistance = curDistance;
2609
+ hasZoomed = false;
2610
+ }
2611
+ }
2612
+ }
2613
+ function pointerup_handler(ev) {
2614
+ // Remove this pointer from the cache and reset the target's
2615
+ // background and border
2616
+ remove_event(ev);
2617
+ // If the number of pointers down is less than two then reset diff tracker
2618
+ if (evCache.length < 2) {
2619
+ origDistance = -1;
2620
+ }
2621
+ }
2622
+ function remove_event(ev) {
2623
+ // Remove this event from the target's cache
2624
+ for (let i = 0; i < evCache.length; i++) {
2625
+ if (evCache[i].pointerId === ev.pointerId) {
2626
+ evCache.splice(i, 1);
2627
+ break;
2628
+ }
2629
+ }
2630
+ }
2631
+ function buttonOver(ev) {
2632
+ const clist = ev.currentTarget.classList;
2633
+ if (clist !== undefined && !clist.contains("disabled"))
2634
+ clist.add("over");
2635
+ ev.redraw = false;
2636
+ }
2637
+ function buttonOut(ev) {
2638
+ const clist = ev.currentTarget.classList;
2639
+ if (clist !== undefined)
2640
+ clist.remove("over");
2641
+ ev.redraw = false;
2642
+ }
2643
+ // Glyphicon utility function: inserts a glyphicon span
2644
+ function glyph(icon, attrs, grayed) {
2645
+ return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
2646
+ }
2647
+ function glyphGrayed(icon, attrs) {
2648
+ return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
2649
+ }
2650
+ // Utility function: inserts non-breaking space
2651
+ function nbsp(n) {
2652
+ return m.trust("&nbsp;");
2653
+ }
2654
+ // Utility functions
2655
+ function escapeHtml(string) {
2656
+ /* Utility function to properly encode a string into HTML */
2657
+ const entityMap = {
2658
+ "&": "&amp;",
2659
+ "<": "&lt;",
2660
+ ">": "&gt;",
2661
+ '"': '&quot;',
2662
+ "'": '&#39;',
2663
+ "/": '&#x2F;'
2664
+ };
2665
+ return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
2666
+ }
2667
+ function getUrlVars(url) {
2668
+ // Get values from a URL query string
2669
+ const hashes = url.split('&');
2670
+ const vars = {};
2671
+ for (let i = 0; i < hashes.length; i++) {
2672
+ const hash = hashes[i].split('=');
2673
+ if (hash.length == 2)
2674
+ vars[hash[0]] = decodeURIComponent(hash[1]);
2675
+ }
2676
+ return vars;
2677
+ }
2678
+ function getInput(id) {
2679
+ // Return the current value of a text input field
2680
+ const elem = document.getElementById(id);
2681
+ return elem.value;
2682
+ }
2683
+ function setInput(id, val) {
2684
+ // Set the current value of a text input field
2685
+ const elem = document.getElementById(id);
2686
+ elem.value = val;
2687
+ }
2688
+ function playAudio(elemId) {
2689
+ // Play an audio file
2690
+ const sound = document.getElementById(elemId);
2691
+ if (sound)
2692
+ sound.play();
2693
+ }
2694
+ function arrayEqual(a, b) {
2695
+ // Return true if arrays a and b are equal
2696
+ if (a.length != b.length)
2697
+ return false;
2698
+ for (let i = 0; i < a.length; i++)
2699
+ if (a[i] != b[i])
2700
+ return false;
2701
+ return true;
2702
+ }
2703
+ function gameUrl(url) {
2704
+ // Convert old-style game URL to new-style single-page URL
2705
+ // The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
2706
+ if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
2707
+ // Cut off "/board?game="
2708
+ url = url.slice(BOARD_PREFIX_LEN);
2709
+ // Isolate the game UUID
2710
+ const uuid = url.slice(0, 36);
2711
+ // Isolate the other parameters, if any
2712
+ let params = url.slice(36);
2713
+ // Start parameter section of URL with a ? sign
2714
+ if (params.length > 0 && params.charAt(0) == "&")
2715
+ params = "?" + params.slice(1);
2716
+ // Return the single-page URL, to be consumed by m.route.Link()
2717
+ return "/game/" + uuid + params;
2718
+ }
2719
+ function scrollMovelistToBottom() {
2720
+ // If the length of the move list has changed,
2721
+ // scroll the last move into view
2722
+ let movelist = document.querySelectorAll("div.movelist .move");
2723
+ if (!movelist || !movelist.length)
2724
+ return;
2725
+ let target = movelist[movelist.length - 1];
2726
+ let parent = target.parentNode;
2727
+ let len = parent.getAttribute("data-len");
2728
+ let intLen = (!len) ? 0 : parseInt(len);
2729
+ if (movelist.length > intLen) {
2730
+ // The list has grown since we last updated it:
2731
+ // scroll to the bottom and mark its length
2732
+ parent.scrollTop = target.offsetTop;
2733
+ }
2734
+ parent.setAttribute("data-len", movelist.length.toString());
2735
+ }
2736
+ function coord(row, col, vertical = false) {
2737
+ // Return the co-ordinate string for the given 0-based row and col
2738
+ if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
2739
+ return null;
2740
+ // Horizontal moves have the row letter first
2741
+ // Vertical moves have the column number first
2742
+ return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
2743
+ }
2744
+ function toVector(co) {
2745
+ // Convert a co-ordinate string to a 0-based row, col and direction vector
2746
+ var dx = 0, dy = 0;
2747
+ var col = 0;
2748
+ var row = ROWIDS.indexOf(co[0]);
2749
+ if (row >= 0) {
2750
+ /* Horizontal move */
2751
+ col = parseInt(co.slice(1)) - 1;
2752
+ dx = 1;
2753
+ }
2754
+ else {
2755
+ /* Vertical move */
2756
+ row = ROWIDS.indexOf(co.slice(-1));
2757
+ col = parseInt(co) - 1;
2758
+ dy = 1;
2759
+ }
2760
+ return { col: col, row: row, dx: dx, dy: dy };
2761
+ }
2762
+ function valueOrK(value, breakpoint = 10000) {
2763
+ // Return a numeric value as a string, but in kilos (thousands)
2764
+ // if it exceeds a breakpoint, in that case suffixed by "K"
2765
+ const sign = value < 0 ? "-" : "";
2766
+ value = Math.abs(value);
2767
+ if (value < breakpoint)
2768
+ return `${sign}${value}`;
2769
+ value = Math.round(value / 1000);
2770
+ return `${sign}${value}K`;
2771
+ }
2772
+ // SalesCloud stuff
2773
+ function doRegisterSalesCloud(i, s, o, g, r, a, m) {
2774
+ i.SalesCloudObject = r;
2775
+ i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
2776
+ i[r].l = 1 * new Date();
2777
+ a = s.createElement(o);
2778
+ m = s.getElementsByTagName(o)[0];
2779
+ a.src = g;
2780
+ m.parentNode.insertBefore(a, m);
2781
+ }
2782
+ function registerSalesCloud() {
2783
+ doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
2784
+ }
2785
+
2239
2786
  /**
2240
2787
  * @license
2241
2788
  * Copyright 2017 Google LLC
@@ -26561,33 +27108,46 @@ let database;
26561
27108
  let analytics;
26562
27109
  function initFirebase(state) {
26563
27110
  try {
26564
- const projectId = state.projectId;
27111
+ const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
26565
27112
  const firebaseOptions = {
26566
27113
  projectId,
26567
- apiKey: state.firebaseAPIKey,
27114
+ apiKey: firebaseAPIKey,
26568
27115
  authDomain: `${projectId}.firebaseapp.com`,
26569
- databaseURL: state.databaseURL,
27116
+ databaseURL,
26570
27117
  storageBucket: `${projectId}.firebasestorage.app`,
26571
- messagingSenderId: state.firebaseSenderId,
26572
- appId: state.firebaseAppId,
26573
- measurementId: state.measurementId,
27118
+ messagingSenderId: firebaseSenderId,
27119
+ appId: firebaseAppId,
27120
+ measurementId,
26574
27121
  };
26575
- app = initializeApp(firebaseOptions, "netskrafl");
27122
+ app = initializeApp(firebaseOptions, projectId);
26576
27123
  if (!app) {
26577
27124
  console.error("Failed to initialize Firebase");
27125
+ return false;
26578
27126
  }
26579
27127
  }
26580
27128
  catch (e) {
26581
27129
  console.error("Failed to initialize Firebase", e);
27130
+ return false;
27131
+ }
27132
+ return true;
27133
+ }
27134
+ function isFirebaseAuthenticated(state) {
27135
+ // Check if Firebase is currently authenticated
27136
+ // If auth is not initialized but state is provided, try to initialize
27137
+ if (!auth) {
27138
+ if (!app)
27139
+ initFirebase(state);
27140
+ if (app)
27141
+ auth = getAuth(app);
26582
27142
  }
27143
+ if (!auth)
27144
+ return false;
27145
+ return auth.currentUser !== null;
26583
27146
  }
26584
27147
  async function loginFirebase(state, firebaseToken, onLoginFunc) {
26585
- if (!app)
26586
- initFirebase(state);
26587
- if (!app)
27148
+ if (!app && !initFirebase(state))
26588
27149
  return;
26589
- const userId = state.userId;
26590
- const locale = state.locale;
27150
+ const { userId, locale, projectId, loginMethod } = state;
26591
27151
  auth = getAuth(app);
26592
27152
  if (!auth) {
26593
27153
  console.error("Failed to initialize Firebase Auth");
@@ -26600,16 +27160,16 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
26600
27160
  // For new users, log an additional signup event
26601
27161
  if (state.newUser) {
26602
27162
  logEvent("sign_up", {
26603
- locale: state.locale,
26604
- method: state.loginMethod,
26605
- userid: state.userId
27163
+ locale,
27164
+ method: loginMethod,
27165
+ userid: userId
26606
27166
  });
26607
27167
  }
26608
27168
  // And always log a login event
26609
27169
  logEvent("login", {
26610
- locale: state.locale,
26611
- method: state.loginMethod,
26612
- userid: state.userId
27170
+ locale,
27171
+ method: loginMethod,
27172
+ userid: userId
26613
27173
  });
26614
27174
  }
26615
27175
  });
@@ -26624,7 +27184,7 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
26624
27184
  if (!analytics) {
26625
27185
  console.error("Failed to initialize Firebase Analytics");
26626
27186
  }
26627
- initPresence(state.projectId, userId, locale);
27187
+ initPresence(projectId, userId, locale);
26628
27188
  }
26629
27189
  function initPresence(projectId, userId, locale) {
26630
27190
  // Ensure that this user connection is recorded in Firebase
@@ -26681,473 +27241,6 @@ function logEvent(ev, params) {
26681
27241
  logEvent$2(analytics, ev, params);
26682
27242
  }
26683
27243
 
26684
- /*
26685
-
26686
- i8n.ts
26687
-
26688
- Single page UI for Netskrafl/Explo using the Mithril library
26689
-
26690
- Copyright (C) 2025 Miðeind ehf.
26691
- Author: Vilhjálmur Þorsteinsson
26692
-
26693
- The Creative Commons Attribution-NonCommercial 4.0
26694
- International Public License (CC-BY-NC 4.0) applies to this software.
26695
- For further information, see https://github.com/mideind/Netskrafl
26696
-
26697
-
26698
- This module contains internationalization (i18n) utility functions,
26699
- allowing for translation of displayed text between languages.
26700
-
26701
- Text messages for individual locales are loaded from the
26702
- /static/assets/messages.json file, which is fetched from the server.
26703
-
26704
- */
26705
- // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
26706
- // This is overwritten in setLocale()
26707
- let currentLocale = "is_IS";
26708
- let currentFallback = "is";
26709
- // Regex that matches embedded interpolations such as "Welcome, {username}!"
26710
- // Interpolation identifiers should only contain ASCII characters, digits and '_'
26711
- const rex = /{\s*(\w+)\s*}/g;
26712
- let messages = {};
26713
- let messagesLoaded = false;
26714
- function hasAnyTranslation(msgs, locale) {
26715
- // Return true if any translation is available for the given locale
26716
- for (let key in msgs) {
26717
- if (msgs[key][locale] !== undefined)
26718
- return true;
26719
- }
26720
- return false;
26721
- }
26722
- function setLocale(locale, msgs) {
26723
- // Set the current i18n locale and fallback
26724
- currentLocale = locale;
26725
- currentFallback = locale.split("_")[0];
26726
- // For unsupported locales, i.e. locales that have no
26727
- // translations available for them, fall back to English (U.S.).
26728
- if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
26729
- currentLocale = "en_US";
26730
- currentFallback = "en";
26731
- }
26732
- // Flatten the Messages structure, enabling long strings
26733
- // to be represented as string arrays in the messages.json file
26734
- messages = {};
26735
- for (let key in msgs) {
26736
- for (let lc in msgs[key]) {
26737
- let s = msgs[key][lc];
26738
- if (Array.isArray(s))
26739
- s = s.join("");
26740
- if (messages[key] === undefined)
26741
- messages[key] = {};
26742
- // If the string s contains HTML markup of the form <tag>...</tag>,
26743
- // convert it into a list of Mithril Vnode children corresponding to
26744
- // the text and the tags
26745
- if (s.match(/<[a-z]+>/)) {
26746
- // Looks like the string contains HTML markup
26747
- const vnodes = [];
26748
- let i = 0;
26749
- let tagMatch = null;
26750
- while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
26751
- // Found what looks like an HTML tag
26752
- // Calculate the index of the enclosed text within s
26753
- const tag = tagMatch[0];
26754
- let j = i + tagMatch.index + tag.length;
26755
- // Find the end tag
26756
- let end = s.indexOf("</" + tag.slice(1), j);
26757
- if (end < 0) {
26758
- // No end tag - skip past this weirdness
26759
- i = j;
26760
- continue;
26761
- }
26762
- // Add the text preceding the tag
26763
- if (tagMatch.index > 0)
26764
- vnodes.push(s.slice(i, i + tagMatch.index));
26765
- // Create the Mithril node corresponding to the tag and the enclosed text
26766
- // and add it to the list
26767
- vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
26768
- // Advance the index past the end of the tag
26769
- i = end + tag.length + 1;
26770
- }
26771
- // Push the final text part, if any
26772
- if (i < s.length)
26773
- vnodes.push(s.slice(i));
26774
- // Reassign s to the list of vnodes
26775
- s = vnodes;
26776
- }
26777
- messages[key][lc] = s;
26778
- }
26779
- }
26780
- messagesLoaded = true;
26781
- }
26782
- async function loadMessages(locale) {
26783
- // Load the internationalization message JSON file from the server
26784
- // and set the user's locale
26785
- try {
26786
- const messages = await request({
26787
- method: "GET",
26788
- url: "/static/assets/messages.json",
26789
- withCredentials: false, // Cookies are not allowed for CORS request
26790
- });
26791
- setLocale(locale, messages);
26792
- }
26793
- catch (_a) {
26794
- setLocale(locale, {});
26795
- }
26796
- }
26797
- function t(key, ips = {}) {
26798
- // Main text translation function, supporting interpolation
26799
- // and HTML tag substitution
26800
- const msgDict = messages[key];
26801
- if (msgDict === undefined)
26802
- // No dictionary for this key - may actually be a missing entry
26803
- return messagesLoaded ? key : "";
26804
- // Lookup exact locale, then fallback, then resort to returning the key
26805
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
26806
- // If we have an interpolation object, do the interpolation first
26807
- return Object.keys(ips).length ? interpolate(message, ips) : message;
26808
- }
26809
- function ts(key, ips = {}) {
26810
- // String translation function, supporting interpolation
26811
- // but not HTML tag substitution
26812
- const msgDict = messages[key];
26813
- if (msgDict === undefined)
26814
- // No dictionary for this key - may actually be a missing entry
26815
- return messagesLoaded ? key : "";
26816
- // Lookup exact locale, then fallback, then resort to returning the key
26817
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
26818
- if (typeof message != "string")
26819
- // This is actually an error - the client should be calling t() instead
26820
- return "";
26821
- // If we have an interpolation object, do the interpolation first
26822
- return Object.keys(ips).length ? interpolate_string(message, ips) : message;
26823
- }
26824
- function mt(cls, children) {
26825
- // Wrapper for the Mithril m() function that auto-translates
26826
- // string and array arguments
26827
- if (typeof children == "string") {
26828
- return m(cls, t(children));
26829
- }
26830
- if (Array.isArray(children)) {
26831
- return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
26832
- }
26833
- return m(cls, children);
26834
- }
26835
- function interpolate(message, ips) {
26836
- // Replace interpolation placeholders with their corresponding values
26837
- if (typeof message == "string") {
26838
- return message.replace(rex, (match, key) => ips[key] || match);
26839
- }
26840
- if (Array.isArray(message)) {
26841
- return message.map((item) => interpolate(item, ips));
26842
- }
26843
- return message;
26844
- }
26845
- function interpolate_string(message, ips) {
26846
- // Replace interpolation placeholders with their corresponding values
26847
- return message.replace(rex, (match, key) => ips[key] || match);
26848
- }
26849
-
26850
- /*
26851
-
26852
- Types.ts
26853
-
26854
- Common type definitions for the Explo/Netskrafl user interface
26855
-
26856
- Copyright (C) 2025 Miðeind ehf.
26857
- Author: Vilhjalmur Thorsteinsson
26858
-
26859
- The Creative Commons Attribution-NonCommercial 4.0
26860
- International Public License (CC-BY-NC 4.0) applies to this software.
26861
- For further information, see https://github.com/mideind/Netskrafl
26862
-
26863
- */
26864
- // Global constants
26865
- const RACK_SIZE = 7;
26866
- const ROWIDS = "ABCDEFGHIJKLMNO";
26867
- const BOARD_SIZE = ROWIDS.length;
26868
- const EXTRA_WIDE_LETTERS = "q";
26869
- const WIDE_LETTERS = "zxmæ";
26870
- const ZOOM_FACTOR = 1.5;
26871
- const ERROR_MESSAGES = {
26872
- // Translations are found in /static/assets/messages.json
26873
- 1: "Enginn stafur lagður niður",
26874
- 2: "Fyrsta orð verður að liggja um byrjunarreitinn",
26875
- 3: "Orð verður að vera samfellt á borðinu",
26876
- 4: "Orð verður að tengjast orði sem fyrir er",
26877
- 5: "Reitur þegar upptekinn",
26878
- 6: "Ekki má vera eyða í orði",
26879
- 7: "word_not_found",
26880
- 8: "word_not_found",
26881
- 9: "Of margir stafir lagðir niður",
26882
- 10: "Stafur er ekki í rekkanum",
26883
- 11: "Of fáir stafir eftir, skipting ekki leyfð",
26884
- 12: "Of mörgum stöfum skipt",
26885
- 13: "Leik vantar á borðið - notið F5/Refresh",
26886
- 14: "Notandi ekki innskráður - notið F5/Refresh",
26887
- 15: "Rangur eða óþekktur notandi",
26888
- 16: "Viðureign finnst ekki",
26889
- 17: "Viðureign er ekki utan tímamarka",
26890
- 18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
26891
- 19: "Véfenging er ekki möguleg í þessari viðureign",
26892
- 20: "Síðasti leikur er ekki véfengjanlegur",
26893
- 21: "Aðeins véfenging eða pass leyfileg",
26894
- "server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
26895
- };
26896
-
26897
- /*
26898
-
26899
- Util.ts
26900
-
26901
- Utility functions for the Explo/Netskrafl user interface
26902
-
26903
- Copyright (C) 2025 Miðeind ehf.
26904
- Author: Vilhjálmur Þorsteinsson
26905
-
26906
- The Creative Commons Attribution-NonCommercial 4.0
26907
- International Public License (CC-BY-NC 4.0) applies to this software.
26908
- For further information, see https://github.com/mideind/Netskrafl
26909
-
26910
- The following code is based on
26911
- https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
26912
-
26913
- */
26914
- // Global vars to cache event state
26915
- var evCache = [];
26916
- var origDistance = -1;
26917
- const PINCH_THRESHOLD = 10; // Minimum pinch movement
26918
- var hasZoomed = false;
26919
- // Old-style (non-single-page) game URL prefix
26920
- const BOARD_PREFIX = "/board?game=";
26921
- const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
26922
- function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
26923
- // Install event handlers for the pointer target
26924
- attrs.onpointerdown = pointerdown_handler;
26925
- attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
26926
- // Use same handler for pointer{up,cancel,out,leave} events since
26927
- // the semantics for these events - in this app - are the same.
26928
- attrs.onpointerup = pointerup_handler;
26929
- attrs.onpointercancel = pointerup_handler;
26930
- attrs.onpointerout = pointerup_handler;
26931
- attrs.onpointerleave = pointerup_handler;
26932
- }
26933
- function pointerdown_handler(ev) {
26934
- // The pointerdown event signals the start of a touch interaction.
26935
- // This event is cached to support 2-finger gestures
26936
- evCache.push(ev);
26937
- }
26938
- function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
26939
- // This function implements a 2-pointer horizontal pinch/zoom gesture.
26940
- //
26941
- // If the distance between the two pointers has increased (zoom in),
26942
- // the target element's background is changed to "pink" and if the
26943
- // distance is decreasing (zoom out), the color is changed to "lightblue".
26944
- //
26945
- // Find this event in the cache and update its record with this event
26946
- for (let i = 0; i < evCache.length; i++) {
26947
- if (ev.pointerId === evCache[i].pointerId) {
26948
- evCache[i] = ev;
26949
- break;
26950
- }
26951
- }
26952
- // If two pointers are down, check for pinch gestures
26953
- if (evCache.length == 2) {
26954
- // Calculate the distance between the two pointers
26955
- const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
26956
- Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
26957
- if (origDistance > 0) {
26958
- if (curDistance - origDistance >= PINCH_THRESHOLD) {
26959
- // The distance between the two pointers has increased
26960
- if (!hasZoomed)
26961
- funcZoomIn();
26962
- hasZoomed = true;
26963
- }
26964
- else if (origDistance - curDistance >= PINCH_THRESHOLD) {
26965
- // The distance between the two pointers has decreased
26966
- if (!hasZoomed)
26967
- funcZoomOut();
26968
- hasZoomed = true;
26969
- }
26970
- }
26971
- else if (origDistance < 0) {
26972
- // Note the original difference between two pointers
26973
- origDistance = curDistance;
26974
- hasZoomed = false;
26975
- }
26976
- }
26977
- }
26978
- function pointerup_handler(ev) {
26979
- // Remove this pointer from the cache and reset the target's
26980
- // background and border
26981
- remove_event(ev);
26982
- // If the number of pointers down is less than two then reset diff tracker
26983
- if (evCache.length < 2) {
26984
- origDistance = -1;
26985
- }
26986
- }
26987
- function remove_event(ev) {
26988
- // Remove this event from the target's cache
26989
- for (let i = 0; i < evCache.length; i++) {
26990
- if (evCache[i].pointerId === ev.pointerId) {
26991
- evCache.splice(i, 1);
26992
- break;
26993
- }
26994
- }
26995
- }
26996
- function buttonOver(ev) {
26997
- const clist = ev.currentTarget.classList;
26998
- if (clist !== undefined && !clist.contains("disabled"))
26999
- clist.add("over");
27000
- ev.redraw = false;
27001
- }
27002
- function buttonOut(ev) {
27003
- const clist = ev.currentTarget.classList;
27004
- if (clist !== undefined)
27005
- clist.remove("over");
27006
- ev.redraw = false;
27007
- }
27008
- // Glyphicon utility function: inserts a glyphicon span
27009
- function glyph(icon, attrs, grayed) {
27010
- return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
27011
- }
27012
- function glyphGrayed(icon, attrs) {
27013
- return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
27014
- }
27015
- // Utility function: inserts non-breaking space
27016
- function nbsp(n) {
27017
- return m.trust("&nbsp;");
27018
- }
27019
- // Utility functions
27020
- function escapeHtml(string) {
27021
- /* Utility function to properly encode a string into HTML */
27022
- const entityMap = {
27023
- "&": "&amp;",
27024
- "<": "&lt;",
27025
- ">": "&gt;",
27026
- '"': '&quot;',
27027
- "'": '&#39;',
27028
- "/": '&#x2F;'
27029
- };
27030
- return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
27031
- }
27032
- function getUrlVars(url) {
27033
- // Get values from a URL query string
27034
- const hashes = url.split('&');
27035
- const vars = {};
27036
- for (let i = 0; i < hashes.length; i++) {
27037
- const hash = hashes[i].split('=');
27038
- if (hash.length == 2)
27039
- vars[hash[0]] = decodeURIComponent(hash[1]);
27040
- }
27041
- return vars;
27042
- }
27043
- function getInput(id) {
27044
- // Return the current value of a text input field
27045
- const elem = document.getElementById(id);
27046
- return elem.value;
27047
- }
27048
- function setInput(id, val) {
27049
- // Set the current value of a text input field
27050
- const elem = document.getElementById(id);
27051
- elem.value = val;
27052
- }
27053
- function playAudio(elemId) {
27054
- // Play an audio file
27055
- const sound = document.getElementById(elemId);
27056
- if (sound)
27057
- sound.play();
27058
- }
27059
- function arrayEqual(a, b) {
27060
- // Return true if arrays a and b are equal
27061
- if (a.length != b.length)
27062
- return false;
27063
- for (let i = 0; i < a.length; i++)
27064
- if (a[i] != b[i])
27065
- return false;
27066
- return true;
27067
- }
27068
- function gameUrl(url) {
27069
- // Convert old-style game URL to new-style single-page URL
27070
- // The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
27071
- if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
27072
- // Cut off "/board?game="
27073
- url = url.slice(BOARD_PREFIX_LEN);
27074
- // Isolate the game UUID
27075
- const uuid = url.slice(0, 36);
27076
- // Isolate the other parameters, if any
27077
- let params = url.slice(36);
27078
- // Start parameter section of URL with a ? sign
27079
- if (params.length > 0 && params.charAt(0) == "&")
27080
- params = "?" + params.slice(1);
27081
- // Return the single-page URL, to be consumed by m.route.Link()
27082
- return "/game/" + uuid + params;
27083
- }
27084
- function scrollMovelistToBottom() {
27085
- // If the length of the move list has changed,
27086
- // scroll the last move into view
27087
- let movelist = document.querySelectorAll("div.movelist .move");
27088
- if (!movelist || !movelist.length)
27089
- return;
27090
- let target = movelist[movelist.length - 1];
27091
- let parent = target.parentNode;
27092
- let len = parent.getAttribute("data-len");
27093
- let intLen = (!len) ? 0 : parseInt(len);
27094
- if (movelist.length > intLen) {
27095
- // The list has grown since we last updated it:
27096
- // scroll to the bottom and mark its length
27097
- parent.scrollTop = target.offsetTop;
27098
- }
27099
- parent.setAttribute("data-len", movelist.length.toString());
27100
- }
27101
- function coord(row, col, vertical = false) {
27102
- // Return the co-ordinate string for the given 0-based row and col
27103
- if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
27104
- return null;
27105
- // Horizontal moves have the row letter first
27106
- // Vertical moves have the column number first
27107
- return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
27108
- }
27109
- function toVector(co) {
27110
- // Convert a co-ordinate string to a 0-based row, col and direction vector
27111
- var dx = 0, dy = 0;
27112
- var col = 0;
27113
- var row = ROWIDS.indexOf(co[0]);
27114
- if (row >= 0) {
27115
- /* Horizontal move */
27116
- col = parseInt(co.slice(1)) - 1;
27117
- dx = 1;
27118
- }
27119
- else {
27120
- /* Vertical move */
27121
- row = ROWIDS.indexOf(co.slice(-1));
27122
- col = parseInt(co) - 1;
27123
- dy = 1;
27124
- }
27125
- return { col: col, row: row, dx: dx, dy: dy };
27126
- }
27127
- function valueOrK(value, breakpoint = 10000) {
27128
- // Return a numeric value as a string, but in kilos (thousands)
27129
- // if it exceeds a breakpoint, in that case suffixed by "K"
27130
- const sign = value < 0 ? "-" : "";
27131
- value = Math.abs(value);
27132
- if (value < breakpoint)
27133
- return `${sign}${value}`;
27134
- value = Math.round(value / 1000);
27135
- return `${sign}${value}K`;
27136
- }
27137
- // SalesCloud stuff
27138
- function doRegisterSalesCloud(i, s, o, g, r, a, m) {
27139
- i.SalesCloudObject = r;
27140
- i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
27141
- i[r].l = 1 * new Date();
27142
- a = s.createElement(o);
27143
- m = s.getElementsByTagName(o)[0];
27144
- a.src = g;
27145
- m.parentNode.insertBefore(a, m);
27146
- }
27147
- function registerSalesCloud() {
27148
- doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
27149
- }
27150
-
27151
27244
  /*
27152
27245
 
27153
27246
  Logo.ts
@@ -27257,7 +27350,7 @@ const NetskraflLegend = (initialVnode) => {
27257
27350
  m.redraw();
27258
27351
  }
27259
27352
  return {
27260
- oncreate: () => {
27353
+ oninit: () => {
27261
27354
  if (msStepTime && ival === 0) {
27262
27355
  ival = setInterval(doStep, msStepTime);
27263
27356
  }
@@ -27312,10 +27405,224 @@ const AnimatedNetskraflLogo = (initialVnode) => {
27312
27405
  };
27313
27406
  };
27314
27407
 
27408
+ /*
27409
+
27410
+ Login.ts
27411
+
27412
+ Login UI for Metskrafl using the Mithril library
27413
+
27414
+ Copyright (C) 2025 Miðeind ehf.
27415
+ Author: Vilhjálmur Þorsteinsson
27416
+
27417
+ The Creative Commons Attribution-NonCommercial 4.0
27418
+ International Public License (CC-BY-NC 4.0) applies to this software.
27419
+ For further information, see https://github.com/mideind/Netskrafl
27420
+
27421
+ This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
27422
+ straightforward JavaScript single-page reactive UI library.
27423
+
27424
+ */
27425
+ const loginUserByEmail = async (state) => {
27426
+ // Call the /login_malstadur endpoint on the server
27427
+ // to log in the user with the given email and token.
27428
+ // The token is a standard HS256-encoded JWT with aud "netskrafl"
27429
+ // and iss typically "malstadur".
27430
+ const { userEmail, userNick, userFullname, token } = state;
27431
+ return requestWithoutAuth(state, {
27432
+ method: "POST",
27433
+ url: "/login_malstadur",
27434
+ body: { email: userEmail, nickname: userNick, fullname: userFullname, token }
27435
+ });
27436
+ };
27437
+ const LoginError = {
27438
+ view: (vnode) => {
27439
+ return m("div.error", {
27440
+ style: { visibility: "visible" }
27441
+ }, vnode.children);
27442
+ }
27443
+ };
27444
+ const LoginForm = (initialVnode) => {
27445
+ const loginUrl = initialVnode.attrs.loginUrl;
27446
+ let loginInProgress = false;
27447
+ function doLogin(ev) {
27448
+ loginInProgress = true;
27449
+ ev.preventDefault();
27450
+ window.location.href = loginUrl;
27451
+ }
27452
+ return {
27453
+ view: () => {
27454
+ return m.fragment({}, [
27455
+ // This is visible on large screens
27456
+ m("div.loginform-large", [
27457
+ m(NetskraflLogoOnly, {
27458
+ className: "login-logo",
27459
+ width: 200,
27460
+ }),
27461
+ m(NetskraflLegend, {
27462
+ className: "login-legend",
27463
+ width: 600,
27464
+ msStepTime: 0
27465
+ }),
27466
+ mt("div.welcome", "welcome_0"),
27467
+ mt("div.welcome", "welcome_1"),
27468
+ mt("div.welcome", "welcome_2"),
27469
+ m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
27470
+ t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
27471
+ ])
27472
+ ]),
27473
+ // This is visible on small screens
27474
+ m("div.loginform-small", [
27475
+ m(NetskraflLogoOnly, {
27476
+ className: "login-logo",
27477
+ width: 160,
27478
+ }),
27479
+ m(NetskraflLegend, {
27480
+ className: "login-legend",
27481
+ width: 650,
27482
+ msStepTime: 0
27483
+ }),
27484
+ m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
27485
+ ])
27486
+ ]);
27487
+ }
27488
+ };
27489
+ };
27490
+
27491
+ // Global state for authentication
27492
+ let authPromise = null;
27493
+ // Custom error class for authentication failures
27494
+ class AuthenticationError extends Error {
27495
+ constructor() {
27496
+ super("Authentication required");
27497
+ this.name = "AuthenticationError";
27498
+ }
27499
+ }
27500
+ // Internal function to ensure authentication
27501
+ const ensureAuthenticated = async (state) => {
27502
+ var _a, _b, _c, _d, _e, _f, _g, _h;
27503
+ // If login is already in progress, wait for it to complete
27504
+ if (authPromise) {
27505
+ await authPromise;
27506
+ return;
27507
+ }
27508
+ // Start new login attempt (either forced by 401 or needed for Firebase)
27509
+ authPromise = loginUserByEmail(state);
27510
+ try {
27511
+ const result = await authPromise;
27512
+ if (result.status === "expired") {
27513
+ // Token has expired, notify the React component if callback is set
27514
+ state.tokenExpired && state.tokenExpired();
27515
+ // Clear any persisted settings since they're no longer valid
27516
+ clearAuthSettings();
27517
+ throw new Error("Token expired");
27518
+ }
27519
+ else if (result.status !== "success") {
27520
+ // Clear any persisted settings on auth failure
27521
+ clearAuthSettings();
27522
+ throw new Error(`Authentication failed: ${result.message || result.status}`);
27523
+ }
27524
+ // Update the user's ID to the internal one used by the backend and Firebase
27525
+ state.userId = result.user_id || state.userId;
27526
+ // Update the user's nickname
27527
+ state.userNick = result.nickname || state.userNick;
27528
+ // Use the server's Firebase API key, if provided
27529
+ state.firebaseAPIKey = result.firebase_api_key || state.firebaseAPIKey;
27530
+ // Load state flags and preferences
27531
+ state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
27532
+ state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
27533
+ state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
27534
+ state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
27535
+ // Save the authentication settings to sessionStorage for persistence
27536
+ saveAuthSettings({
27537
+ userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
27538
+ userId: state.userId,
27539
+ userNick: state.userNick,
27540
+ firebaseAPIKey: state.firebaseAPIKey,
27541
+ beginner: state.beginner,
27542
+ fairPlay: state.fairPlay,
27543
+ ready: state.ready,
27544
+ readyTimed: state.readyTimed,
27545
+ });
27546
+ // Success: Log in to Firebase with the token passed from the server
27547
+ await loginFirebase(state, result.firebase_token);
27548
+ }
27549
+ finally {
27550
+ // Reset the promise so future 401s can trigger a new login
27551
+ authPromise = null;
27552
+ }
27553
+ };
27554
+ // Internal authenticated request function
27555
+ const authenticatedRequest = async (state, options, retries = 0) => {
27556
+ // Before making the request, check if Firebase needs authentication
27557
+ // This handles the case where the user returns with a valid backend cookie but expired Firebase auth
27558
+ if (!retries && !isFirebaseAuthenticated(state)) {
27559
+ // Firebase is not authenticated, ensure authentication before making the request
27560
+ await ensureAuthenticated(state);
27561
+ }
27562
+ try {
27563
+ // Make the actual request
27564
+ // console.log("Making authenticated request to", options.url);
27565
+ return await m.request({
27566
+ withCredentials: true,
27567
+ ...options,
27568
+ url: serverUrl(state, options.url),
27569
+ extract: (xhr) => {
27570
+ // Check for 401 Unauthorized
27571
+ if (xhr.status === 401) {
27572
+ // console.log("Received 401 Unauthorized, triggering re-authentication");
27573
+ throw new AuthenticationError();
27574
+ }
27575
+ // Handle empty responses
27576
+ if (!xhr.responseText) {
27577
+ return null;
27578
+ }
27579
+ // Parse JSON response
27580
+ try {
27581
+ return JSON.parse(xhr.responseText);
27582
+ }
27583
+ catch (e) {
27584
+ // If JSON parsing fails, return the raw text
27585
+ return xhr.responseText;
27586
+ }
27587
+ }
27588
+ });
27589
+ }
27590
+ catch (error) {
27591
+ if (error instanceof AuthenticationError && !retries) {
27592
+ // Backend returned 401, perform full authentication and retry (force=true)
27593
+ await ensureAuthenticated(state);
27594
+ // Retry the original request
27595
+ return authenticatedRequest(state, options, retries + 1);
27596
+ }
27597
+ // Re-throw other errors
27598
+ throw error;
27599
+ }
27600
+ };
27601
+ const request = (state, options) => {
27602
+ // Enhanced request with automatic authentication handling
27603
+ return authenticatedRequest(state, options);
27604
+ };
27605
+ const requestMoves = (state, options) => {
27606
+ // Call the moves service on the Google App Engine backend
27607
+ const url = `${state.movesUrl}${options.url}`;
27608
+ const headers = {
27609
+ "Content-Type": "application/json; charset=UTF-8",
27610
+ Authorization: `Bearer ${state.movesAccessKey}`,
27611
+ ...options === null || options === void 0 ? void 0 : options.headers,
27612
+ };
27613
+ return m.request({
27614
+ withCredentials: false,
27615
+ method: "POST",
27616
+ ...options,
27617
+ url,
27618
+ headers,
27619
+ });
27620
+ };
27621
+
27315
27622
  const SPINNER_INITIAL_DELAY = 800; // milliseconds
27316
27623
  const Spinner = {
27317
27624
  // Show a spinner wait box, after an initial delay
27318
- oncreate: (vnode) => {
27625
+ oninit: (vnode) => {
27319
27626
  vnode.state.show = false;
27320
27627
  vnode.state.ival = setTimeout(() => {
27321
27628
  vnode.state.show = true;
@@ -27428,11 +27735,12 @@ const OnlinePresence = (initialVnode) => {
27428
27735
  const askServer = attrs.online === undefined;
27429
27736
  const id = attrs.id;
27430
27737
  const userId = attrs.userId;
27738
+ const state = attrs.state;
27431
27739
  let loading = false;
27432
27740
  async function _update() {
27433
27741
  if (askServer && !loading) {
27434
27742
  loading = true;
27435
- const json = await request({
27743
+ const json = await request(state, {
27436
27744
  method: "POST",
27437
27745
  url: "/onlinecheck",
27438
27746
  body: { user: userId }
@@ -27442,7 +27750,7 @@ const OnlinePresence = (initialVnode) => {
27442
27750
  }
27443
27751
  }
27444
27752
  return {
27445
- oncreate: _update,
27753
+ oninit: _update,
27446
27754
  view: (vnode) => {
27447
27755
  var _a, _b;
27448
27756
  if (!askServer)
@@ -27708,6 +28016,7 @@ const WaitDialog = (initialVnode) => {
27708
28016
  const attrs = initialVnode.attrs;
27709
28017
  const view = attrs.view;
27710
28018
  const model = view.model;
28019
+ const state = model.state;
27711
28020
  const duration = attrs.duration;
27712
28021
  const oppId = attrs.oppId;
27713
28022
  const key = attrs.challengeKey;
@@ -27723,9 +28032,9 @@ const WaitDialog = (initialVnode) => {
27723
28032
  async function updateOnline() {
27724
28033
  // Initiate an online check on the opponent
27725
28034
  try {
27726
- if (!oppId || !key)
28035
+ if (!oppId || !key || !state)
27727
28036
  return;
27728
- const json = await request({
28037
+ const json = await request(state, {
27729
28038
  method: "POST",
27730
28039
  url: "/initwait",
27731
28040
  body: { opp: oppId, key }
@@ -27741,8 +28050,10 @@ const WaitDialog = (initialVnode) => {
27741
28050
  }
27742
28051
  async function cancelWait() {
27743
28052
  // Cancel a pending wait for a timed game
28053
+ if (!state)
28054
+ return;
27744
28055
  try {
27745
- await request({
28056
+ await request(state, {
27746
28057
  method: "POST",
27747
28058
  url: "/cancelwait",
27748
28059
  body: {
@@ -27776,11 +28087,13 @@ const WaitDialog = (initialVnode) => {
27776
28087
  return {
27777
28088
  oncreate,
27778
28089
  view: () => {
28090
+ if (!state)
28091
+ return null;
27779
28092
  return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
27780
28093
  m(".chall-hdr", m("table", m("tbody", m("tr", [
27781
28094
  m("td", m("h1.chall-icon", glyph("time"))),
27782
28095
  m("td.l-border", [
27783
- m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline }),
28096
+ m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
27784
28097
  m("h1", oppNick),
27785
28098
  m("h2", oppName)
27786
28099
  ])
@@ -27823,6 +28136,7 @@ const AcceptDialog = (initialVnode) => {
27823
28136
  // is linked up with her opponent and a new game is started
27824
28137
  const attrs = initialVnode.attrs;
27825
28138
  const view = attrs.view;
28139
+ const state = view.model.state;
27826
28140
  const oppId = attrs.oppId;
27827
28141
  const key = attrs.challengeKey;
27828
28142
  let oppNick = attrs.oppNick;
@@ -27830,11 +28144,11 @@ const AcceptDialog = (initialVnode) => {
27830
28144
  let loading = false;
27831
28145
  async function waitCheck() {
27832
28146
  // Initiate a wait status check on the opponent
27833
- if (loading)
28147
+ if (loading || !state)
27834
28148
  return; // Already checking
27835
28149
  loading = true;
27836
28150
  try {
27837
- const json = await request({
28151
+ const json = await request(state, {
27838
28152
  method: "POST",
27839
28153
  url: "/waitcheck",
27840
28154
  body: { user: oppId, key }
@@ -27857,7 +28171,7 @@ const AcceptDialog = (initialVnode) => {
27857
28171
  }
27858
28172
  }
27859
28173
  return {
27860
- oncreate: waitCheck,
28174
+ oninit: waitCheck,
27861
28175
  view: () => {
27862
28176
  return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
27863
28177
  m(".chall-hdr", m("table", m("tbody", m("tr", [
@@ -28042,87 +28356,6 @@ const FriendCancelConfirmDialog = (initialVnode) => {
28042
28356
  };
28043
28357
  };
28044
28358
 
28045
- /*
28046
-
28047
- Login.ts
28048
-
28049
- Login UI for Metskrafl using the Mithril library
28050
-
28051
- Copyright (C) 2025 Miðeind ehf.
28052
- Author: Vilhjálmur Þorsteinsson
28053
-
28054
- The Creative Commons Attribution-NonCommercial 4.0
28055
- International Public License (CC-BY-NC 4.0) applies to this software.
28056
- For further information, see https://github.com/mideind/Netskrafl
28057
-
28058
- This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
28059
- straightforward JavaScript single-page reactive UI library.
28060
-
28061
- */
28062
- const loginUserByEmail = async (email, nickname, fullname, token) => {
28063
- // Call the /login_malstadur endpoint on the server
28064
- // to log in the user with the given email and token.
28065
- // The token is a standard HS256-encoded JWT with aud "netskrafl"
28066
- // and iss typically "malstadur".
28067
- return request({
28068
- method: "POST",
28069
- url: "/login_malstadur",
28070
- body: { email, nickname, fullname, token }
28071
- });
28072
- };
28073
- const LoginError = {
28074
- view: (vnode) => {
28075
- var _a;
28076
- return m("div.error", { style: { visibility: "visible" } }, ((_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.message) || "Error logging in");
28077
- }
28078
- };
28079
- const LoginForm = (initialVnode) => {
28080
- const loginUrl = initialVnode.attrs.loginUrl;
28081
- let loginInProgress = false;
28082
- function doLogin(ev) {
28083
- loginInProgress = true;
28084
- ev.preventDefault();
28085
- window.location.href = loginUrl;
28086
- }
28087
- return {
28088
- view: () => {
28089
- return m.fragment({}, [
28090
- // This is visible on large screens
28091
- m("div.loginform-large", [
28092
- m(NetskraflLogoOnly, {
28093
- className: "login-logo",
28094
- width: 200,
28095
- }),
28096
- m(NetskraflLegend, {
28097
- className: "login-legend",
28098
- width: 600,
28099
- msStepTime: 0
28100
- }),
28101
- mt("div.welcome", "welcome_0"),
28102
- mt("div.welcome", "welcome_1"),
28103
- mt("div.welcome", "welcome_2"),
28104
- m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
28105
- t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
28106
- ])
28107
- ]),
28108
- // This is visible on small screens
28109
- m("div.loginform-small", [
28110
- m(NetskraflLogoOnly, {
28111
- className: "login-logo",
28112
- width: 160,
28113
- }),
28114
- m(NetskraflLegend, {
28115
- className: "login-legend",
28116
- width: 650,
28117
- msStepTime: 0
28118
- }),
28119
- m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
28120
- ])
28121
- ]);
28122
- }
28123
- };
28124
- };
28125
-
28126
28359
  /*
28127
28360
 
28128
28361
  ChallengeDialog.ts
@@ -28153,7 +28386,11 @@ const ChallengeDialog = () => {
28153
28386
  m(".chall-hdr", m("table", m("tbody", m("tr", [
28154
28387
  m("td", m("h1.chall-icon", glyph("hand-right"))),
28155
28388
  m("td.l-border", [
28156
- m(OnlinePresence, { id: "chall-online", userId: item.userid }),
28389
+ m(OnlinePresence, {
28390
+ id: "chall-online",
28391
+ userId: item.userid,
28392
+ state,
28393
+ }),
28157
28394
  m("h1", item.nick),
28158
28395
  m("h2", item.fullname)
28159
28396
  ])
@@ -29469,7 +29706,7 @@ const PromoDialog = (initialVnode) => {
29469
29706
  initFunc();
29470
29707
  }
29471
29708
  return {
29472
- oncreate: _fetchContent,
29709
+ oninit: _fetchContent,
29473
29710
  view: (vnode) => {
29474
29711
  let initFunc = vnode.attrs.initFunc;
29475
29712
  return m(".modal-dialog", { id: "promo-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "promo-form", className: "promo-" + vnode.attrs.kind }, m("div", {
@@ -29541,7 +29778,7 @@ const UserInfoDialog = (initialVnode) => {
29541
29778
  }
29542
29779
  }
29543
29780
  return {
29544
- oncreate: (vnode) => {
29781
+ oninit: (vnode) => {
29545
29782
  _updateRecentList(vnode);
29546
29783
  _updateStats(vnode);
29547
29784
  },
@@ -30531,7 +30768,9 @@ const Board = (initialVnode) => {
30531
30768
  const scale = view.boardScale || 1.0;
30532
30769
  let attrs = {};
30533
30770
  // Add handlers for pinch zoom functionality
30534
- addPinchZoom(attrs, view.zoomIn, view.zoomOut);
30771
+ // Note: resist the temptation to pass zoomIn/zoomOut directly,
30772
+ // as that would not bind the 'this' pointer correctly
30773
+ addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
30535
30774
  if (scale !== 1.0)
30536
30775
  attrs.style = `transform: scale(${scale})`;
30537
30776
  return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
@@ -30709,7 +30948,7 @@ const Chat = (initialVnode) => {
30709
30948
  const { view } = initialVnode.attrs;
30710
30949
  const model = view.model;
30711
30950
  const game = model.game;
30712
- model.state;
30951
+ const state = model.state;
30713
30952
  function decodeTimestamp(ts) {
30714
30953
  // Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
30715
30954
  return {
@@ -30766,7 +31005,7 @@ const Chat = (initialVnode) => {
30766
31005
  for (const emoticon of EMOTICONS)
30767
31006
  if (str.indexOf(emoticon.icon) >= 0) {
30768
31007
  // The string contains the emoticon: prepare to replace all occurrences
30769
- const imgUrl = serverUrl(emoticon.image);
31008
+ const imgUrl = serverUrl(state, emoticon.image);
30770
31009
  const img = `<img src='${imgUrl}' height='32' width='32'>`;
30771
31010
  // Re the following trick, see https://stackoverflow.com/questions/1144783/
30772
31011
  // replacing-all-occurrences-of-a-string-in-javascript
@@ -31917,13 +32156,13 @@ class View {
31917
32156
  views.push(this.vwLogin());
31918
32157
  break;
31919
32158
  case "loginerror":
31920
- views.push(m(LoginError));
32159
+ views.push(m(LoginError, { key: "login-error" }, t("Villa við innskráningu")));
31921
32160
  break;
31922
32161
  case "main":
31923
- views.push(m(Main, { view: this }));
32162
+ views.push(m(Main, { key: "main", view: this }));
31924
32163
  break;
31925
32164
  case "game":
31926
- views.push(m(GameView, { view: this }));
32165
+ views.push(m(GameView, { key: "game", view: this }));
31927
32166
  break;
31928
32167
  case "review":
31929
32168
  const n = vwReview(this);
@@ -31931,7 +32170,7 @@ class View {
31931
32170
  break;
31932
32171
  case "thanks":
31933
32172
  // Display a thank-you dialog on top of the normal main screen
31934
- views.push(m(Main, { view: this }));
32173
+ views.push(m(Main, { key: "main", view: this }));
31935
32174
  // Be careful to add the Thanks dialog only once to the stack
31936
32175
  if (!this.dialogStack.length)
31937
32176
  this.showThanks();
@@ -31942,7 +32181,7 @@ class View {
31942
32181
  views.push(this.vwHelp(parseInt(m.route.param("tab") || ""), parseInt(m.route.param("faq") || "")));
31943
32182
  break;
31944
32183
  default:
31945
- return [m("div", t("Þessi vefslóð er ekki rétt"))];
32184
+ return [m("div", { key: "error" }, t("Þessi vefslóð er ekki rétt"))];
31946
32185
  }
31947
32186
  // Push any open dialogs
31948
32187
  for (const dialog of this.dialogStack) {
@@ -31957,7 +32196,7 @@ class View {
31957
32196
  }
31958
32197
  // Overlay a spinner, if active
31959
32198
  if (model.spinners)
31960
- views.push(m(Spinner));
32199
+ views.push(m(Spinner, { key: "spinner" }));
31961
32200
  return views;
31962
32201
  }
31963
32202
  // Dialog support
@@ -32038,7 +32277,7 @@ class View {
32038
32277
  zoomOut() {
32039
32278
  if (this.boardScale !== 1.0) {
32040
32279
  this.boardScale = 1.0;
32041
- setTimeout(this.resetScale);
32280
+ setTimeout(() => this.resetScale());
32042
32281
  }
32043
32282
  }
32044
32283
  resetScale() {
@@ -32085,7 +32324,7 @@ class View {
32085
32324
  // No game or we're in full screen mode: always 100% scale
32086
32325
  // Also, as soon as a move is being processed by the server, we zoom out
32087
32326
  this.boardScale = 1.0; // Needs to be done before setTimeout() call
32088
- setTimeout(this.resetScale);
32327
+ setTimeout(() => this.resetScale());
32089
32328
  return;
32090
32329
  }
32091
32330
  const tp = game.tilesPlaced();
@@ -32179,7 +32418,7 @@ class View {
32179
32418
  }
32180
32419
  }
32181
32420
  // Output literal HTML obtained from rawhelp.html on the server
32182
- return m.fragment({}, [
32421
+ return m.fragment({ key: "help" }, [
32183
32422
  m(LeftLogo),
32184
32423
  m(UserId, { view: this }),
32185
32424
  this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
@@ -32225,6 +32464,7 @@ class View {
32225
32464
  vnode.dom.querySelector("#nickname").focus();
32226
32465
  }
32227
32466
  return m(".modal-dialog", {
32467
+ key: "userprefs",
32228
32468
  id: "user-dialog",
32229
32469
  oncreate: initFocus
32230
32470
  // onupdate: initFocus
@@ -32306,11 +32546,12 @@ class View {
32306
32546
  model.loadUser(true); // Activate spinner while loading
32307
32547
  if (!model.user)
32308
32548
  // Nothing to edit (the spinner should be showing in this case)
32309
- return m.fragment({}, []);
32549
+ return m.fragment({ key: "userprefs-empty" }, []);
32310
32550
  return this.vwUserPrefsDialog();
32311
32551
  }
32312
32552
  vwUserInfo(args) {
32313
32553
  return m(UserInfoDialog, {
32554
+ key: "userinfodialog-" + args.userid,
32314
32555
  view: this,
32315
32556
  userid: args.userid,
32316
32557
  nick: args.nick,
@@ -32319,6 +32560,7 @@ class View {
32319
32560
  }
32320
32561
  vwPromo(args) {
32321
32562
  return m(PromoDialog, {
32563
+ key: "promo-" + args.kind,
32322
32564
  view: this,
32323
32565
  kind: args.kind,
32324
32566
  initFunc: args.initFunc
@@ -32326,6 +32568,7 @@ class View {
32326
32568
  }
32327
32569
  vwWait(args) {
32328
32570
  return m(WaitDialog, {
32571
+ key: "wait-" + args.challengeKey,
32329
32572
  view: this,
32330
32573
  oppId: args.oppId,
32331
32574
  oppNick: args.oppNick,
@@ -32336,6 +32579,7 @@ class View {
32336
32579
  }
32337
32580
  vwAccept(args) {
32338
32581
  return m(AcceptDialog, {
32582
+ key: "accept-" + args.challengeKey,
32339
32583
  view: this,
32340
32584
  oppId: args.oppId,
32341
32585
  oppNick: args.oppNick,
@@ -32346,7 +32590,7 @@ class View {
32346
32590
  var _a;
32347
32591
  const model = this.model;
32348
32592
  const loginUrl = ((_a = model.state) === null || _a === void 0 ? void 0 : _a.loginUrl) || "";
32349
- return m(LoginForm, { loginUrl });
32593
+ return m(LoginForm, { key: "login", loginUrl });
32350
32594
  }
32351
32595
  vwDialogs() {
32352
32596
  // Show prompt dialogs below game board, if any
@@ -32425,12 +32669,12 @@ class View {
32425
32669
  View.dialogViews = {
32426
32670
  userprefs: (view) => view.vwUserPrefs(),
32427
32671
  userinfo: (view, args) => view.vwUserInfo(args),
32428
- challenge: (view, args) => m(ChallengeDialog, { view, item: args }),
32672
+ challenge: (view, args) => m(ChallengeDialog, { key: "challenge-" + args.item.challenge_key, view, item: args }),
32429
32673
  promo: (view, args) => view.vwPromo(args),
32430
- friend: (view) => m(FriendPromoteDialog, { view }),
32431
- thanks: (view) => m(FriendThanksDialog, { view }),
32432
- cancel: (view) => m(FriendCancelDialog, { view }),
32433
- confirm: (view) => m(FriendCancelConfirmDialog, { view }),
32674
+ friend: (view) => m(FriendPromoteDialog, { key: "friend", view }),
32675
+ thanks: (view) => m(FriendThanksDialog, { key: "thanks", view }),
32676
+ cancel: (view) => m(FriendCancelDialog, { key: "cancel", view }),
32677
+ confirm: (view) => m(FriendCancelConfirmDialog, { key: "confirm", view }),
32434
32678
  wait: (view, args) => view.vwWait(args),
32435
32679
  accept: (view, args) => view.vwAccept(args)
32436
32680
  };
@@ -32468,7 +32712,7 @@ class WordChecker {
32468
32712
  }
32469
32713
  }
32470
32714
  }
32471
- async checkWords(locale, words) {
32715
+ async checkWords(state, locale, words) {
32472
32716
  // Return true if all words are valid in the given locale,
32473
32717
  // or false otherwise. Lookups are cached for efficiency.
32474
32718
  let cache = this.wordCheckCache[locale];
@@ -32496,7 +32740,7 @@ class WordChecker {
32496
32740
  }
32497
32741
  // We need a server roundtrip
32498
32742
  try {
32499
- const response = await requestMoves({
32743
+ const response = await requestMoves(state, {
32500
32744
  url: "/wordcheck",
32501
32745
  body: {
32502
32746
  locale,
@@ -32726,7 +32970,7 @@ const BOARD = {
32726
32970
  }
32727
32971
  };
32728
32972
  class BaseGame {
32729
- constructor(uuid, board_type = "standard") {
32973
+ constructor(uuid, state, board_type = "standard") {
32730
32974
  // Basic game properties that don't change while the game is underway
32731
32975
  this.locale = "is_IS";
32732
32976
  this.alphabet = "";
@@ -32760,6 +33004,7 @@ class BaseGame {
32760
33004
  this.board_type = board_type;
32761
33005
  this.startSquare = START_SQUARE[board_type];
32762
33006
  this.startCoord = START_COORD[board_type];
33007
+ this.state = state;
32763
33008
  }
32764
33009
  // Default init method that can be overridden
32765
33010
  init() {
@@ -33000,7 +33245,7 @@ class BaseGame {
33000
33245
  if (!this.manual) {
33001
33246
  // This is not a manual-wordcheck game:
33002
33247
  // Check the word that has been laid down
33003
- const found = await wordChecker.checkWords(this.locale, scoreResult.words);
33248
+ const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
33004
33249
  this.wordGood = found;
33005
33250
  this.wordBad = !found;
33006
33251
  }
@@ -33263,10 +33508,10 @@ const MAX_OVERTIME = 10 * 60.0;
33263
33508
  const DEBUG_OVERTIME = 1 * 60.0;
33264
33509
  const GAME_OVER = 99; // Error code corresponding to the Error class in skraflmechanics.py
33265
33510
  class Game extends BaseGame {
33266
- constructor(uuid, srvGame, moveListener, maxOvertime) {
33511
+ constructor(uuid, srvGame, moveListener, state, maxOvertime) {
33267
33512
  var _a;
33268
33513
  // Call parent constructor
33269
- super(uuid); // Default board_type: "standard"
33514
+ super(uuid, state); // Default board_type: "standard"
33270
33515
  // A class that represents a Netskrafl game instance on the client
33271
33516
  // Netskrafl-specific properties
33272
33517
  this.userid = ["", ""];
@@ -33523,7 +33768,7 @@ class Game extends BaseGame {
33523
33768
  try {
33524
33769
  if (!this.uuid)
33525
33770
  return;
33526
- const result = await request({
33771
+ const result = await request(this.state, {
33527
33772
  method: "POST",
33528
33773
  url: "/gamestate",
33529
33774
  body: { game: this.uuid } // !!! FIXME: Add delete_zombie parameter
@@ -33726,7 +33971,7 @@ class Game extends BaseGame {
33726
33971
  this.chatLoading = true;
33727
33972
  this.messages = [];
33728
33973
  try {
33729
- const result = await request({
33974
+ const result = await request(this.state, {
33730
33975
  method: "POST",
33731
33976
  url: "/chatload",
33732
33977
  body: { channel: "game:" + this.uuid }
@@ -33752,7 +33997,7 @@ class Game extends BaseGame {
33752
33997
  // Load statistics about a game
33753
33998
  this.stats = undefined; // Error/in-progress status
33754
33999
  try {
33755
- const json = await request({
34000
+ const json = await request(this.state, {
33756
34001
  method: "POST",
33757
34002
  url: "/gamestats",
33758
34003
  body: { game: this.uuid }
@@ -33772,7 +34017,7 @@ class Game extends BaseGame {
33772
34017
  async sendMessage(msg) {
33773
34018
  // Send a chat message
33774
34019
  try {
33775
- await request({
34020
+ await request(this.state, {
33776
34021
  method: "POST",
33777
34022
  url: "/chatmsg",
33778
34023
  body: { channel: "game:" + this.uuid, msg: msg }
@@ -33974,10 +34219,10 @@ class Game extends BaseGame {
33974
34219
  // Send a move to the server
33975
34220
  this.moveInProgress = true;
33976
34221
  try {
33977
- const result = await request({
34222
+ const result = await request(this.state, {
33978
34223
  method: "POST",
33979
34224
  url: "/submitmove",
33980
- body: { moves: moves, mcount: this.moves.length, uuid: this.uuid }
34225
+ body: { moves: moves, mcount: this.moves.length, uuid: this.uuid },
33981
34226
  });
33982
34227
  // The update() function also handles error results
33983
34228
  this.update(result);
@@ -34003,7 +34248,7 @@ class Game extends BaseGame {
34003
34248
  // Force resignation by a tardy opponent
34004
34249
  this.moveInProgress = true;
34005
34250
  try {
34006
- const result = await request({
34251
+ const result = await request(this.state, {
34007
34252
  method: "POST",
34008
34253
  url: "/forceresign",
34009
34254
  body: { mcount: this.moves.length, game: this.uuid }
@@ -34127,7 +34372,10 @@ const HOT_WARM_BOUNDARY_RATIO = 0.6;
34127
34372
  const WARM_COLD_BOUNDARY_RATIO = 0.3;
34128
34373
  class Riddle extends BaseGame {
34129
34374
  constructor(uuid, date, model) {
34130
- super(uuid);
34375
+ if (!model.state) {
34376
+ throw new Error("No global state in Riddle constructor");
34377
+ }
34378
+ super(uuid, model.state);
34131
34379
  // Scoring properties, static
34132
34380
  this.bestPossibleScore = 0;
34133
34381
  this.warmBoundary = 0;
@@ -34168,9 +34416,12 @@ class Riddle extends BaseGame {
34168
34416
  async load(date, locale) {
34169
34417
  this.date = date;
34170
34418
  this.locale = locale;
34419
+ const { state } = this;
34171
34420
  try {
34421
+ if (!state)
34422
+ throw new Error("No global state in Riddle.load");
34172
34423
  // Request riddle data from server (HTTP API call)
34173
- const response = await request({
34424
+ const response = await request(state, {
34174
34425
  method: "POST",
34175
34426
  url: "/gatadagsins/riddle",
34176
34427
  body: { date, locale }
@@ -34198,11 +34449,11 @@ class Riddle extends BaseGame {
34198
34449
  }
34199
34450
  }
34200
34451
  async submitRiddleWord(move) {
34201
- const { state } = this.model;
34452
+ const { state } = this;
34202
34453
  if (!state || !state.userId)
34203
34454
  return;
34204
34455
  try {
34205
- await request({
34456
+ await request(state, {
34206
34457
  method: "POST",
34207
34458
  url: "/gatadagsins/submit",
34208
34459
  body: {
@@ -34482,8 +34733,6 @@ function getSettings() {
34482
34733
  }
34483
34734
  class Model {
34484
34735
  constructor(settings, state) {
34485
- // A class for the underlying data model, displayed by the current view
34486
- this.state = null;
34487
34736
  this.paths = [];
34488
34737
  // The routeName will be "login", "main", "game"...
34489
34738
  this.routeName = undefined;
@@ -34542,7 +34791,54 @@ class Model {
34542
34791
  this.isExplo = state.isExplo;
34543
34792
  this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
34544
34793
  // Load localized text messages from the messages.json file
34545
- loadMessages(state.locale);
34794
+ loadMessages(state, state.locale);
34795
+ }
34796
+ // Simple POST request with JSON body (most common case)
34797
+ async post(url, body) {
34798
+ if (!this.state) {
34799
+ throw new Error("Model state is not initialized");
34800
+ }
34801
+ return request(this.state, {
34802
+ method: "POST",
34803
+ url,
34804
+ body
34805
+ });
34806
+ }
34807
+ // GET request for HTML/text content
34808
+ async getText(url) {
34809
+ if (!this.state) {
34810
+ throw new Error("Model state is not initialized");
34811
+ }
34812
+ return request(this.state, {
34813
+ method: "GET",
34814
+ url,
34815
+ responseType: "text",
34816
+ deserialize: (str) => str
34817
+ });
34818
+ }
34819
+ // POST request that returns text/HTML
34820
+ async postText(url, body) {
34821
+ if (!this.state) {
34822
+ throw new Error("Model state is not initialized");
34823
+ }
34824
+ return request(this.state, {
34825
+ method: "POST",
34826
+ url,
34827
+ body,
34828
+ responseType: "text",
34829
+ deserialize: (str) => str
34830
+ });
34831
+ }
34832
+ // Request to the moves service
34833
+ async postMoves(body) {
34834
+ if (!this.state) {
34835
+ throw new Error("Model state is not initialized");
34836
+ }
34837
+ return requestMoves(this.state, {
34838
+ method: "POST",
34839
+ url: "/moves",
34840
+ body
34841
+ });
34546
34842
  }
34547
34843
  async loadGame(uuid, funcComplete, deleteZombie = false) {
34548
34844
  var _a;
@@ -34561,20 +34857,16 @@ class Model {
34561
34857
  this.highlightedMove = null;
34562
34858
  if (!uuid)
34563
34859
  return; // Should not happen
34564
- const result = await request({
34565
- method: "POST",
34566
- url: "/gamestate",
34567
- body: {
34568
- game: uuid,
34569
- delete_zombie: deleteZombie
34570
- }
34860
+ const result = await this.post("/gamestate", {
34861
+ game: uuid,
34862
+ delete_zombie: deleteZombie
34571
34863
  });
34572
34864
  if (!(result === null || result === void 0 ? void 0 : result.ok)) {
34573
34865
  // console.log("Game " + uuid + " could not be loaded");
34574
34866
  }
34575
34867
  else {
34576
34868
  // Create a new game instance and load the state into it
34577
- this.game = new Game(uuid, result.game, this, ((_a = this.state) === null || _a === void 0 ? void 0 : _a.runningLocal) ? DEBUG_OVERTIME : MAX_OVERTIME);
34869
+ this.game = new Game(uuid, result.game, this, this.state, ((_a = this.state) === null || _a === void 0 ? void 0 : _a.runningLocal) ? DEBUG_OVERTIME : MAX_OVERTIME);
34578
34870
  // Successfully loaded: call the completion function, if given
34579
34871
  // (this usually attaches the Firebase event listener)
34580
34872
  funcComplete && funcComplete();
@@ -34597,11 +34889,7 @@ class Model {
34597
34889
  this.numChallenges = 0;
34598
34890
  this.oppReady = 0;
34599
34891
  try {
34600
- const json = await request({
34601
- method: "POST",
34602
- url: "/allgamelists",
34603
- body: { zombie: includeZombies, count: 40 }
34604
- });
34892
+ const json = await this.post("/allgamelists", { zombie: includeZombies, count: 40 });
34605
34893
  if (!json || json.result !== 0) {
34606
34894
  // An error occurred
34607
34895
  this.gameList = [];
@@ -34643,11 +34931,7 @@ class Model {
34643
34931
  this.numGames = 0;
34644
34932
  this.spinners++;
34645
34933
  try {
34646
- const json = await request({
34647
- method: "POST",
34648
- url: "/gamelist",
34649
- body: { zombie: includeZombies }
34650
- });
34934
+ const json = await this.post("/gamelist", { zombie: includeZombies });
34651
34935
  if (!json || json.result !== 0) {
34652
34936
  // An error occurred
34653
34937
  this.gameList = [];
@@ -34677,10 +34961,7 @@ class Model {
34677
34961
  this.oppReady = 0;
34678
34962
  this.spinners++; // Show spinner while loading
34679
34963
  try {
34680
- const json = await request({
34681
- method: "POST",
34682
- url: "/challengelist"
34683
- });
34964
+ const json = await this.post("/challengelist");
34684
34965
  if (!json || json.result !== 0) {
34685
34966
  // An error occurred
34686
34967
  this.challengeList = [];
@@ -34714,11 +34995,7 @@ class Model {
34714
34995
  this.recentList = [];
34715
34996
  this.spinners++; // Show spinner while loading
34716
34997
  try {
34717
- const json = await request({
34718
- method: "POST",
34719
- url: "/recentlist",
34720
- body: { versus: null, count: 40 }
34721
- });
34998
+ const json = await this.post("/recentlist", { versus: null, count: 40 });
34722
34999
  if (!json || json.result !== 0) {
34723
35000
  // An error occurred
34724
35001
  this.recentList = [];
@@ -34737,11 +35014,7 @@ class Model {
34737
35014
  }
34738
35015
  async loadUserRecentList(userid, versus, readyFunc) {
34739
35016
  // Load the list of recent games for the given user
34740
- const json = await request({
34741
- method: "POST",
34742
- url: "/recentlist",
34743
- body: { user: userid, versus: versus, count: 40 }
34744
- });
35017
+ const json = await this.post("/recentlist", { user: userid, versus: versus, count: 40 });
34745
35018
  readyFunc(json);
34746
35019
  }
34747
35020
  async loadUserList(criteria) {
@@ -34759,11 +35032,7 @@ class Model {
34759
35032
  const url = "/userlist";
34760
35033
  const body = criteria;
34761
35034
  try {
34762
- const json = await request({
34763
- method: "POST",
34764
- url,
34765
- body,
34766
- });
35035
+ const json = await this.post(url, body);
34767
35036
  if (!json || json.result !== 0) {
34768
35037
  // An error occurred
34769
35038
  this.userList = [];
@@ -34785,11 +35054,7 @@ class Model {
34785
35054
  const url = "/rating";
34786
35055
  const body = { kind: spec };
34787
35056
  try {
34788
- const json = await request({
34789
- method: "POST",
34790
- url,
34791
- body,
34792
- });
35057
+ const json = await this.post(url, body);
34793
35058
  if (!json || json.result !== 0) {
34794
35059
  // An error occurred
34795
35060
  this.eloRatingList = [];
@@ -34808,11 +35073,7 @@ class Model {
34808
35073
  // Load statistics for the current user
34809
35074
  this.ownStats = {};
34810
35075
  try {
34811
- const json = await request({
34812
- method: "POST",
34813
- url: "/userstats",
34814
- body: {} // Current user is implicit
34815
- });
35076
+ const json = await this.post("/userstats", {});
34816
35077
  if (!json || json.result !== 0) {
34817
35078
  // An error occurred
34818
35079
  return;
@@ -34825,11 +35086,7 @@ class Model {
34825
35086
  async loadUserStats(userid, readyFunc) {
34826
35087
  // Load statistics for the given user
34827
35088
  try {
34828
- const json = await request({
34829
- method: "POST",
34830
- url: "/userstats",
34831
- body: { user: userid }
34832
- });
35089
+ const json = await this.post("/userstats", { user: userid });
34833
35090
  readyFunc(json);
34834
35091
  }
34835
35092
  catch (e) {
@@ -34839,13 +35096,7 @@ class Model {
34839
35096
  async loadPromoContent(key, readyFunc) {
34840
35097
  // Load HTML content for promo dialog
34841
35098
  try {
34842
- const html = await request({
34843
- method: "POST",
34844
- url: "/promo",
34845
- body: { key: key },
34846
- responseType: "text",
34847
- deserialize: (str) => str
34848
- });
35099
+ const html = await this.postText("/promo", { key: key });
34849
35100
  readyFunc(html);
34850
35101
  }
34851
35102
  catch (e) {
@@ -34894,11 +35145,7 @@ class Model {
34894
35145
  rack,
34895
35146
  limit: NUM_BEST_MOVES,
34896
35147
  };
34897
- const json = await requestMoves({
34898
- method: "POST",
34899
- url: "/moves",
34900
- body: rq,
34901
- });
35148
+ const json = await this.postMoves(rq);
34902
35149
  this.highlightedMove = null;
34903
35150
  if (!json || json.moves === undefined) {
34904
35151
  // Something unexpected going on
@@ -34933,12 +35180,7 @@ class Model {
34933
35180
  return; // Already loaded
34934
35181
  try {
34935
35182
  const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
34936
- const result = await request({
34937
- method: "GET",
34938
- url: "/rawhelp?version=malstadur&locale=" + locale,
34939
- responseType: "text",
34940
- deserialize: (str) => str
34941
- });
35183
+ const result = await this.getText("/rawhelp?version=malstadur&locale=" + locale);
34942
35184
  this.helpHTML = result;
34943
35185
  }
34944
35186
  catch (e) {
@@ -34953,12 +35195,7 @@ class Model {
34953
35195
  return; // Already loaded
34954
35196
  try {
34955
35197
  const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
34956
- const result = await request({
34957
- method: "GET",
34958
- url: "/friend?locale=" + locale,
34959
- responseType: "text",
34960
- deserialize: (str) => str
34961
- });
35198
+ const result = await this.getText("/friend?locale=" + locale);
34962
35199
  this.friendHTML = result;
34963
35200
  }
34964
35201
  catch (e) {
@@ -34974,10 +35211,7 @@ class Model {
34974
35211
  this.spinners++;
34975
35212
  }
34976
35213
  try {
34977
- const result = await request({
34978
- method: "POST",
34979
- url: "/loaduserprefs",
34980
- });
35214
+ const result = await this.post("/loaduserprefs");
34981
35215
  if (!result || !result.ok) {
34982
35216
  this.user = null;
34983
35217
  this.userErrors = null;
@@ -35004,11 +35238,7 @@ class Model {
35004
35238
  if (!user)
35005
35239
  return;
35006
35240
  try {
35007
- const result = await request({
35008
- method: "POST",
35009
- url: "/saveuserprefs",
35010
- body: user
35011
- });
35241
+ const result = await this.post("/saveuserprefs", user);
35012
35242
  if (result === null || result === void 0 ? void 0 : result.ok) {
35013
35243
  // User preferences modified successfully on the server:
35014
35244
  // update the state variables that we're caching
@@ -35017,6 +35247,7 @@ class Model {
35017
35247
  state.userNick = user.nickname;
35018
35248
  state.beginner = user.beginner;
35019
35249
  state.fairPlay = user.fairplay;
35250
+ saveAuthSettings(state);
35020
35251
  }
35021
35252
  // Note that state.plan is updated via a Firebase notification
35022
35253
  // Give the game instance a chance to update its state
@@ -35051,41 +35282,22 @@ class Model {
35051
35282
  return false;
35052
35283
  }
35053
35284
  handleUserMessage(json, firstAttach) {
35054
- var _a;
35055
35285
  // Handle an incoming Firebase user message, i.e. a message
35056
35286
  // on the /user/[userid] path
35057
- if (firstAttach || !this.state)
35287
+ if (firstAttach || !this.state || !json)
35058
35288
  return;
35059
35289
  let redraw = false;
35060
- if (json.friend !== undefined) {
35061
- // Potential change of user friendship status
35062
- const newFriend = json.friend ? true : false;
35063
- if (this.user && this.user.friend != newFriend) {
35064
- this.user.friend = newFriend;
35065
- redraw = true;
35066
- }
35067
- }
35068
- if (json.plan !== undefined) {
35290
+ if (typeof json.plan === "string") {
35069
35291
  // Potential change of user subscription plan
35070
- if (this.state.plan != json.plan) {
35292
+ if (this.state.plan !== json.plan) {
35071
35293
  this.state.plan = json.plan;
35072
35294
  redraw = true;
35073
35295
  }
35074
- if (this.user && !this.user.friend && this.state.plan == "friend") {
35075
- // plan == "friend" implies that user.friend should be true
35076
- this.user.friend = true;
35077
- redraw = true;
35078
- }
35079
- if (this.state.plan == "" && ((_a = this.user) === null || _a === void 0 ? void 0 : _a.friend)) {
35080
- // Conversely, an empty plan string means that the user is not a friend
35081
- this.user.friend = false;
35082
- redraw = true;
35083
- }
35084
35296
  }
35085
35297
  if (json.hasPaid !== undefined) {
35086
35298
  // Potential change of payment status
35087
- const newHasPaid = (this.state.plan != "" && json.hasPaid) ? true : false;
35088
- if (this.state.hasPaid != newHasPaid) {
35299
+ const newHasPaid = (this.state.plan !== "" && json.hasPaid) ? true : false;
35300
+ if (this.state.hasPaid !== newHasPaid) {
35089
35301
  this.state.hasPaid = newHasPaid;
35090
35302
  redraw = true;
35091
35303
  }
@@ -35422,8 +35634,10 @@ class Actions {
35422
35634
  }
35423
35635
  async markFavorite(userId, status) {
35424
35636
  // Mark or de-mark a user as a favorite
35637
+ if (!this.model.state)
35638
+ return;
35425
35639
  try {
35426
- await request({
35640
+ await request(this.model.state, {
35427
35641
  method: "POST",
35428
35642
  url: "/favorite",
35429
35643
  body: { destuser: userId, action: status ? "add" : "delete" }
@@ -35441,8 +35655,10 @@ class Actions {
35441
35655
  async handleChallenge(parameters) {
35442
35656
  var _a;
35443
35657
  // Reject or retract a challenge
35658
+ if (!this.model.state)
35659
+ return;
35444
35660
  try {
35445
- const json = await request({
35661
+ const json = await request(this.model.state, {
35446
35662
  method: "POST",
35447
35663
  url: "/challenge",
35448
35664
  body: parameters
@@ -35497,6 +35713,8 @@ class Actions {
35497
35713
  async startNewGame(oppid, reverse = false) {
35498
35714
  var _a;
35499
35715
  // Ask the server to initiate a new game against the given opponent
35716
+ if (!this.model.state)
35717
+ return;
35500
35718
  try {
35501
35719
  const rqBody = { opp: oppid, rev: reverse };
35502
35720
  if (this.model.isExplo) {
@@ -35509,7 +35727,7 @@ class Actions {
35509
35727
  url: "/initgame",
35510
35728
  body: rqBody
35511
35729
  };
35512
- const json = await request(rq);
35730
+ const json = await request(this.model.state, rq);
35513
35731
  if (json === null || json === void 0 ? void 0 : json.ok) {
35514
35732
  // Log the new game event
35515
35733
  const locale = ((_a = this.model.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
@@ -35575,12 +35793,16 @@ class Actions {
35575
35793
  // User Preference Management Actions
35576
35794
  async setUserPref(pref) {
35577
35795
  // Set a user preference on the server
35796
+ if (!this.model.state)
35797
+ return;
35578
35798
  try {
35579
- await request({
35799
+ await request(this.model.state, {
35580
35800
  method: "POST",
35581
35801
  url: "/setuserpref",
35582
35802
  body: pref
35583
35803
  }); // No result required or expected
35804
+ // Update the persisted settings in sessionStorage
35805
+ saveAuthSettings(this.model.state);
35584
35806
  }
35585
35807
  catch (e) {
35586
35808
  // A future TODO might be to signal an error in the UI
@@ -35593,7 +35815,7 @@ class Actions {
35593
35815
  if (!user || !state)
35594
35816
  return false;
35595
35817
  try {
35596
- const json = await request({
35818
+ const json = await request(state, {
35597
35819
  method: "POST",
35598
35820
  url: "/cancelplan",
35599
35821
  body: {}
@@ -35711,41 +35933,25 @@ async function main$1(state, container) {
35711
35933
  console.error("No container element found");
35712
35934
  return "error";
35713
35935
  }
35714
- // Set up Netskrafl backend server URLs
35715
- setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
35716
35936
  try {
35717
- const loginData = await loginUserByEmail(state.userEmail, state.userNick, state.userFullname, state.token);
35718
- if (loginData.status === "expired") {
35719
- // The current Málstaður JWT has expired;
35720
- // we need to obtain a new one
35721
- return "expired";
35722
- }
35723
- if (loginData.status === "success") {
35724
- state.userId = loginData.user_id;
35725
- // Use the nickname from the server, if available
35726
- state.userNick = loginData.nickname || state.userNick;
35727
- // Log in to Firebase with the token passed from the server
35728
- await loginFirebase(state, loginData.firebase_token);
35729
- // Everything looks OK:
35730
- // Create the model, actions and view objects in proper sequence
35731
- const settings = getSettings();
35732
- const model = new Model(settings, state);
35733
- const actions = new Actions(model);
35734
- const view = new View(actions);
35735
- // Run the Mithril router
35736
- const routeResolver = createRouteResolver(actions, view);
35737
- m.route(container, settings.defaultRoute, routeResolver);
35738
- return "success";
35739
- }
35937
+ // Skip initial login - authentication will happen lazily on first API call
35938
+ // Create the model, actions and view objects in proper sequence
35939
+ const settings = getSettings();
35940
+ const model = new Model(settings, state);
35941
+ const actions = new Actions(model);
35942
+ const view = new View(actions);
35943
+ // Run the Mithril router
35944
+ const routeResolver = createRouteResolver(actions, view);
35945
+ m.route(container, settings.defaultRoute, routeResolver);
35740
35946
  }
35741
35947
  catch (e) {
35742
- console.error("Exception during login: ", e);
35948
+ console.error("Exception during initialization: ", e);
35949
+ return "error";
35743
35950
  }
35744
- m.mount(container, LoginError);
35745
- return "error";
35951
+ return "success";
35746
35952
  }
35747
35953
 
35748
- const mountForUser$1 = async (state, tokenExpired) => {
35954
+ const mountForUser$1 = async (state) => {
35749
35955
  // Return a DOM tree containing a mounted Netskrafl UI
35750
35956
  // for the user specified in the state object
35751
35957
  const { userEmail } = state;
@@ -35773,18 +35979,12 @@ const mountForUser$1 = async (state, tokenExpired) => {
35773
35979
  root.className = "netskrafl-user";
35774
35980
  return root;
35775
35981
  }
35776
- else if (loginResult === "expired") {
35777
- // We need a new token from the Málstaður backend
35778
- root.className = "netskrafl-expired";
35779
- tokenExpired && tokenExpired(); // This causes a reload of the component
35780
- return root;
35781
- }
35782
35982
  // console.error("Failed to mount Netskrafl UI for user", userEmail);
35783
35983
  throw new Error("Failed to mount Netskrafl UI");
35784
35984
  };
35785
35985
  const NetskraflImpl = ({ state, tokenExpired }) => {
35786
35986
  const ref = React.createRef();
35787
- const completeState = { ...DEFAULT_STATE, ...state };
35987
+ const completeState = makeGlobalState({ ...state, tokenExpired });
35788
35988
  const { userEmail } = completeState;
35789
35989
  /*
35790
35990
  useEffect(() => {
@@ -35817,7 +36017,7 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
35817
36017
  return;
35818
36018
  }
35819
36019
  try {
35820
- mountForUser$1(completeState, tokenExpired).then((div) => {
36020
+ mountForUser$1(completeState).then((div) => {
35821
36021
  // Attach the div as a child of the container
35822
36022
  // instead of any previous children
35823
36023
  const container = ref.current;
@@ -35899,6 +36099,10 @@ const RiddleScore = {
35899
36099
  else {
35900
36100
  classes.push(".hot");
35901
36101
  }
36102
+ // Add celebration class if the player achieved the best possible score
36103
+ if (score >= riddle.bestPossibleScore) {
36104
+ classes.push(".celebrate");
36105
+ }
35902
36106
  }
35903
36107
  return m("div" + classes.join(""), m("span.gata-dagsins-legend", displayText));
35904
36108
  }
@@ -36240,6 +36444,84 @@ const GataDagsinsRightSide = {
36240
36444
  }
36241
36445
  };
36242
36446
 
36447
+ /*
36448
+
36449
+ gatadagsins-help.ts
36450
+
36451
+ Help dialog for Gáta Dagsins
36452
+
36453
+ Copyright (C) 2025 Miðeind ehf.
36454
+ Author: Vilhjálmur Þorsteinsson
36455
+
36456
+ The Creative Commons Attribution-NonCommercial 4.0
36457
+ International Public License (CC-BY-NC 4.0) applies to this software.
36458
+ For further information, see https://github.com/mideind/Netskrafl
36459
+
36460
+ */
36461
+ const GataDagsinsHelp = {
36462
+ view: (vnode) => {
36463
+ const closeHelp = vnode.attrs.onClose;
36464
+ return m(".modal-dialog.gatadagsins-help", m(".modal-content", [
36465
+ // Header with close button
36466
+ m(".modal-header", [
36467
+ m("h2", "Um Gátu dagsins"),
36468
+ m("button.close", {
36469
+ onclick: closeHelp,
36470
+ "aria-label": "Loka"
36471
+ }, m("span", { "aria-hidden": "true" }, "×"))
36472
+ ]),
36473
+ // Body with help content
36474
+ m(".modal-body", [
36475
+ m("p", "Gáta dagsins er dagleg krossgátuþraut, svipuð skrafli, þar sem þú reynir að finna " +
36476
+ "stigahæsta orðið sem hægt er að mynda með gefnum stöfum."),
36477
+ m("h3", "Hvernig á að spila"),
36478
+ m("ul", [
36479
+ m("li", "Þú færð borð með allmörgum stöfum sem þegar hafa verið lagðir."),
36480
+ m("li", "Neðst á skjánum eru stafaflísar sem þú getur notað til að mynda orð."),
36481
+ m("li", "Dragðu flísar á borðið til að mynda orð, annaðhvort lárétt eða lóðrétt."),
36482
+ m("li", "Orðin verða að tengjast við stafi sem fyrir eru á borðinu."),
36483
+ m("li", "Þú sérð jafnóðum hvort lögnin á borðinu er gild og hversu mörg stig hún gefur."),
36484
+ m("li", "Þú getur prófað eins mörg orð og þú vilt - besta skorið þitt er vistað."),
36485
+ ]),
36486
+ m("h3", "Stigagjöf"),
36487
+ m("p", "Þú færð stig fyrir hvern staf í orðinu, auk bónusstiga fyrir lengri orð:"),
36488
+ m("ul", [
36489
+ m("li", "Hver stafur gefur 1-10 stig eftir gildi hans"),
36490
+ m("li", "Orð sem nota allar 7 stafaflísarnar gefa 50 stiga bónus"),
36491
+ m("li", "Sumir reitir á borðinu tvöfalda eða þrefalda stafagildið"),
36492
+ m("li", "Sumir reitir tvöfalda eða þrefalda heildarorðagildið"),
36493
+ ]),
36494
+ m("h3", "Hitamælir"),
36495
+ m("p", "Hitamælirinn hægra megin (eða efst á farsímum) sýnir:"),
36496
+ m("ul", [
36497
+ m("li", m("strong", "Besta mögulega skor:"), " Hæstu stig sem hægt er að ná á þessu borði."),
36498
+ m("li", m("strong", "Besta skor dagsins:"), " Hæstu stig sem einhver leikmaður hefur náð í dag."),
36499
+ m("li", m("strong", "Þín bestu orð:"), " Orðin sem þú hefur lagt og stigin fyrir þau."),
36500
+ m("li", "Þú getur smellt á orð á hitamælinum til að fá þá lögn aftur á borðið."),
36501
+ ]),
36502
+ m("h3", "Ábendingar"),
36503
+ m("ul", [
36504
+ m("li", "Reyndu að nota dýra stafi (eins og X, Ý, Þ) á tvöföldunar- eða þreföldunarreitum."),
36505
+ m("li", "Lengri orð gefa mun fleiri stig vegna bónussins."),
36506
+ m("li", "Þú getur dregið allar flísar til baka með bláa endurkalls-hnappnum."),
36507
+ m("li", "Ný gáta birtist á hverjum nýjum degi - klukkan 00:00!"),
36508
+ ]),
36509
+ m("h3", "Um leikinn"),
36510
+ m("p", [
36511
+ "Gáta dagsins er systkini ",
36512
+ m("a", { href: "https://netskrafl.is", target: "_blank" }, "Netskrafls"),
36513
+ ", hins sívinsæla íslenska krossgátuleiks á netinu. ",
36514
+ "Leikurinn er þróaður af Miðeind ehf."
36515
+ ]),
36516
+ ]),
36517
+ // Footer with close button
36518
+ m(".modal-footer", m("button.btn.btn-primary", {
36519
+ onclick: closeHelp
36520
+ }, "Loka"))
36521
+ ]));
36522
+ }
36523
+ };
36524
+
36243
36525
  /*
36244
36526
 
36245
36527
  GataDagsins.ts
@@ -36308,47 +36590,65 @@ const currentMoveState = (riddle) => {
36308
36590
  }
36309
36591
  return { selectedMoves, bestMove };
36310
36592
  };
36311
- const GataDagsins$1 = {
36593
+ const GataDagsins$1 = () => {
36312
36594
  // A view of the Gáta Dagsins page
36313
- oninit: (vnode) => {
36314
- const { model, actions } = vnode.attrs.view;
36315
- const { riddle } = model;
36316
- if (!riddle) {
36317
- const { date, locale } = vnode.attrs;
36318
- // Initialize a fresh riddle object if it doesn't exist
36319
- actions.fetchRiddle(date, locale);
36595
+ let showHelp = false;
36596
+ return {
36597
+ oninit: (vnode) => {
36598
+ const { model, actions } = vnode.attrs.view;
36599
+ const { riddle } = model;
36600
+ if (!riddle) {
36601
+ const { date, locale } = vnode.attrs;
36602
+ // Initialize a fresh riddle object if it doesn't exist
36603
+ actions.fetchRiddle(date, locale);
36604
+ }
36605
+ // Initialize help dialog state
36606
+ showHelp = false;
36607
+ },
36608
+ view: (vnode) => {
36609
+ var _a;
36610
+ const { view } = vnode.attrs;
36611
+ const { model } = view;
36612
+ const { riddle } = model;
36613
+ const { selectedMoves, bestMove } = (riddle
36614
+ ? currentMoveState(riddle)
36615
+ : { selectedMoves: [], bestMove: undefined });
36616
+ const toggleHelp = () => {
36617
+ showHelp = !showHelp;
36618
+ m.redraw();
36619
+ };
36620
+ return m("div.drop-target", {
36621
+ id: "gatadagsins-background",
36622
+ }, [
36623
+ // The main content area
36624
+ riddle ? m(".gatadagsins-container", [
36625
+ // Main display area with flex layout
36626
+ m(".gatadagsins-main", [
36627
+ // Board and rack component (left side)
36628
+ m(GataDagsinsBoardAndRack, { view }),
36629
+ // Right-side component with scores and comparisons
36630
+ m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
36631
+ // Blank dialog
36632
+ riddle.askingForBlank
36633
+ ? m(BlankDialog, { game: riddle })
36634
+ : "",
36635
+ ])
36636
+ ]) : "",
36637
+ // The left margin elements: back button and info/help button
36638
+ // These elements appear after the main container for proper z-order
36639
+ // m(LeftLogo), // Currently no need for the logo for Gáta Dagsins
36640
+ // Show the Beginner component if the user is a beginner
36641
+ ((_a = model.state) === null || _a === void 0 ? void 0 : _a.beginner) ? m(Beginner, { view }) : "",
36642
+ // Custom Info button for GataDagsins that shows help dialog
36643
+ m(".info", { title: ts("Upplýsingar og hjálp") }, m("a.iconlink", { href: "#", onclick: (e) => { e.preventDefault(); toggleHelp(); } }, glyph("info-sign"))),
36644
+ // Help dialog and backdrop
36645
+ showHelp ? [
36646
+ m(".modal-backdrop", { onclick: (e) => { e.preventDefault(); } }),
36647
+ m(GataDagsinsHelp, { onClose: toggleHelp })
36648
+ ] : "",
36649
+ ]);
36320
36650
  }
36321
- },
36322
- view: (vnode) => {
36323
- const { view } = vnode.attrs;
36324
- const { model } = view;
36325
- const { riddle } = model;
36326
- const { selectedMoves, bestMove } = (riddle
36327
- ? currentMoveState(riddle)
36328
- : { selectedMoves: [], bestMove: undefined });
36329
- return m("div.drop-target", {
36330
- id: "gatadagsins-background",
36331
- }, [
36332
- // The main content area
36333
- riddle ? m(".gatadagsins-container", [
36334
- // Main display area with flex layout
36335
- m(".gatadagsins-main", [
36336
- // Board and rack component (left side)
36337
- m(GataDagsinsBoardAndRack, { view }),
36338
- // Right-side component with scores and comparisons
36339
- m(GataDagsinsRightSide, { view, selectedMoves, bestMove }),
36340
- // Blank dialog
36341
- riddle.askingForBlank
36342
- ? m(BlankDialog, { game: riddle })
36343
- : "",
36344
- ])
36345
- ]) : "",
36346
- // The left margin elements: back button and info/help button
36347
- // These elements appear after the main container for proper z-order
36348
- m(LeftLogo),
36349
- m(Info),
36350
- ]);
36351
- }
36651
+ };
36352
36652
  };
36353
36653
 
36354
36654
  /*
@@ -36377,44 +36677,28 @@ async function main(state, container) {
36377
36677
  console.error("No container element found");
36378
36678
  return "error";
36379
36679
  }
36380
- // Set up Netskrafl backend server URLs
36381
- setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
36382
36680
  try {
36383
- const loginData = await loginUserByEmail(state.userEmail, state.userNick, state.userFullname, state.token);
36384
- if (loginData.status === "expired") {
36385
- // The current Málstaður JWT has expired;
36386
- // we need to obtain a new one
36387
- return "expired";
36388
- }
36389
- if (loginData.status === "success") {
36390
- state.userId = loginData.user_id;
36391
- // Use the nickname from the server, if available
36392
- state.userNick = loginData.nickname || state.userNick;
36393
- // Log in to Firebase with the token passed from the server
36394
- await loginFirebase(state, loginData.firebase_token);
36395
- // Everything looks OK:
36396
- // Create the model, view and actions objects
36397
- const settings = getSettings();
36398
- const model = new Model(settings, state);
36399
- const actions = new Actions(model);
36400
- const view = new View(actions);
36401
- const today = new Date().toISOString().split("T")[0];
36402
- const locale = state.locale || "is_IS";
36403
- // Mount the Gáta Dagsins UI using an anonymous closure component
36404
- m.mount(container, {
36405
- view: () => m(GataDagsins$1, { view, date: today, locale }),
36406
- });
36407
- return "success";
36408
- }
36681
+ // Skip initial login - authentication will happen lazily on first API call
36682
+ // Create the model, view and actions objects
36683
+ const settings = getSettings();
36684
+ const model = new Model(settings, state);
36685
+ const actions = new Actions(model);
36686
+ const view = new View(actions);
36687
+ const today = new Date().toISOString().split("T")[0];
36688
+ const locale = state.locale || "is_IS";
36689
+ // Mount the Gáta Dagsins UI using an anonymous closure component
36690
+ m.mount(container, {
36691
+ view: () => m(GataDagsins$1, { view, date: today, locale }),
36692
+ });
36409
36693
  }
36410
36694
  catch (e) {
36411
- console.error("Exception during login: ", e);
36695
+ console.error("Exception during initialization: ", e);
36696
+ return "error";
36412
36697
  }
36413
- m.mount(container, LoginError);
36414
- return "error";
36698
+ return "success";
36415
36699
  }
36416
36700
 
36417
- const mountForUser = async (state, tokenExpired) => {
36701
+ const mountForUser = async (state) => {
36418
36702
  // Return a DOM tree containing a mounted Gáta Dagsins UI
36419
36703
  // for the user specified in the state object
36420
36704
  const { userEmail } = state;
@@ -36442,18 +36726,12 @@ const mountForUser = async (state, tokenExpired) => {
36442
36726
  root.className = "gatadagsins-user";
36443
36727
  return root;
36444
36728
  }
36445
- else if (loginResult === "expired") {
36446
- // We need a new token from the Málstaður backend
36447
- root.className = "gatadagsins-expired";
36448
- tokenExpired && tokenExpired(); // This causes a reload of the component
36449
- return root;
36450
- }
36451
36729
  // console.error("Failed to mount Gáta Dagsins UI for user", userEmail);
36452
36730
  throw new Error("Failed to mount Gáta Dagsins UI");
36453
36731
  };
36454
36732
  const GataDagsinsImpl = ({ state, tokenExpired }) => {
36455
36733
  const ref = React.createRef();
36456
- const completeState = { ...DEFAULT_STATE, ...state };
36734
+ const completeState = makeGlobalState({ ...state, tokenExpired });
36457
36735
  const { userEmail } = completeState;
36458
36736
  React.useEffect(() => {
36459
36737
  var _a;
@@ -36471,7 +36749,7 @@ const GataDagsinsImpl = ({ state, tokenExpired }) => {
36471
36749
  return;
36472
36750
  }
36473
36751
  try {
36474
- mountForUser(completeState, tokenExpired).then((div) => {
36752
+ mountForUser(completeState).then((div) => {
36475
36753
  // Attach the div as a child of the container
36476
36754
  // instead of any previous children
36477
36755
  const container = ref.current;