@mideind/netskrafl-react 1.0.0-beta.8 → 1.0.1

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/esm/index.js CHANGED
@@ -31,6 +31,27 @@ const DEFAULT_STATE = {
31
31
  uiLandscape: false,
32
32
  runningLocal: false,
33
33
  };
34
+ const makeServerUrls = (backendUrl, movesUrl) => {
35
+ // If the last character of the url is a slash, cut it off,
36
+ // since path URLs always start with a slash
37
+ const cleanupUrl = (url) => {
38
+ if (url.length > 0 && url[url.length - 1] === "/") {
39
+ url = url.slice(0, -1);
40
+ }
41
+ return url;
42
+ };
43
+ return {
44
+ serverUrl: cleanupUrl(backendUrl),
45
+ movesUrl: cleanupUrl(movesUrl),
46
+ };
47
+ };
48
+ const makeGlobalState = (overrides) => {
49
+ const state = {
50
+ ...DEFAULT_STATE,
51
+ ...overrides,
52
+ };
53
+ return { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
54
+ };
34
55
 
35
56
  function getDefaultExportFromCjs (x) {
36
57
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -2187,52 +2208,485 @@ function requireMithril () {
2187
2208
  var mithrilExports = requireMithril();
2188
2209
  var m = /*@__PURE__*/getDefaultExportFromCjs(mithrilExports);
2189
2210
 
2190
- let BACKEND_SERVER_PREFIX = "http://127.0.0.1:3000"; // Default for development
2191
- let MOVES_SERVER_PREFIX = "https://moves-dot-explo-dev.appspot.com"; // Default for development
2192
- let MOVES_ACCESS_KEY = "None";
2193
- const setServerUrl = (backendUrl, movesUrl, movesAccessKey) => {
2194
- // If the last character of the url is a slash, cut it off,
2195
- // since path URLs always start with a slash
2196
- const cleanupUrl = (url) => {
2197
- if (url.length > 0 && url[url.length - 1] === "/") {
2198
- url = url.slice(0, -1);
2199
- }
2200
- return url;
2201
- };
2202
- if (backendUrl)
2203
- BACKEND_SERVER_PREFIX = cleanupUrl(backendUrl);
2204
- if (movesUrl)
2205
- MOVES_SERVER_PREFIX = cleanupUrl(movesUrl);
2206
- if (movesAccessKey)
2207
- MOVES_ACCESS_KEY = movesAccessKey;
2208
- };
2209
- const serverUrl = (path) => {
2210
- return `${BACKEND_SERVER_PREFIX}${path}`;
2211
+ const serverUrl = (state, path) => {
2212
+ return `${state.serverUrl}${path}`;
2211
2213
  };
2212
- const request = (options) => {
2213
- // Call the default service on the Google App Engine backend
2214
+ // Export a non-authenticated request for use by login.ts to avoid circular dependency
2215
+ const requestWithoutAuth = (state, options) => {
2214
2216
  return m.request({
2215
2217
  withCredentials: true,
2216
2218
  ...options,
2217
- url: serverUrl(options.url),
2219
+ url: serverUrl(state, options.url),
2218
2220
  });
2219
2221
  };
2220
- const requestMoves = (options) => {
2221
- // Call the moves service on the Google App Engine backend
2222
- const headers = {
2223
- "Content-Type": "application/json; charset=UTF-8",
2224
- Authorization: `Bearer ${MOVES_ACCESS_KEY}`,
2225
- ...options === null || options === void 0 ? void 0 : options.headers,
2226
- };
2227
- return m.request({
2228
- withCredentials: false,
2229
- method: "POST",
2230
- ...options,
2231
- url: `${MOVES_SERVER_PREFIX}${options.url}`,
2232
- headers,
2233
- });
2222
+
2223
+ /*
2224
+
2225
+ i8n.ts
2226
+
2227
+ Single page UI for Netskrafl/Explo using the Mithril library
2228
+
2229
+ Copyright (C) 2025 Miðeind ehf.
2230
+ Author: Vilhjálmur Þorsteinsson
2231
+
2232
+ The Creative Commons Attribution-NonCommercial 4.0
2233
+ International Public License (CC-BY-NC 4.0) applies to this software.
2234
+ For further information, see https://github.com/mideind/Netskrafl
2235
+
2236
+
2237
+ This module contains internationalization (i18n) utility functions,
2238
+ allowing for translation of displayed text between languages.
2239
+
2240
+ Text messages for individual locales are loaded from the
2241
+ /static/assets/messages.json file, which is fetched from the server.
2242
+
2243
+ */
2244
+ // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
2245
+ // This is overwritten in setLocale()
2246
+ let currentLocale = "is_IS";
2247
+ let currentFallback = "is";
2248
+ // Regex that matches embedded interpolations such as "Welcome, {username}!"
2249
+ // Interpolation identifiers should only contain ASCII characters, digits and '_'
2250
+ const rex = /{\s*(\w+)\s*}/g;
2251
+ let messages = {};
2252
+ let messagesLoaded = false;
2253
+ function hasAnyTranslation(msgs, locale) {
2254
+ // Return true if any translation is available for the given locale
2255
+ for (let key in msgs) {
2256
+ if (msgs[key][locale] !== undefined)
2257
+ return true;
2258
+ }
2259
+ return false;
2260
+ }
2261
+ function setLocale(locale, msgs) {
2262
+ // Set the current i18n locale and fallback
2263
+ currentLocale = locale;
2264
+ currentFallback = locale.split("_")[0];
2265
+ // For unsupported locales, i.e. locales that have no
2266
+ // translations available for them, fall back to English (U.S.).
2267
+ if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
2268
+ currentLocale = "en_US";
2269
+ currentFallback = "en";
2270
+ }
2271
+ // Flatten the Messages structure, enabling long strings
2272
+ // to be represented as string arrays in the messages.json file
2273
+ messages = {};
2274
+ for (let key in msgs) {
2275
+ for (let lc in msgs[key]) {
2276
+ let s = msgs[key][lc];
2277
+ if (Array.isArray(s))
2278
+ s = s.join("");
2279
+ if (messages[key] === undefined)
2280
+ messages[key] = {};
2281
+ // If the string s contains HTML markup of the form <tag>...</tag>,
2282
+ // convert it into a list of Mithril Vnode children corresponding to
2283
+ // the text and the tags
2284
+ if (s.match(/<[a-z]+>/)) {
2285
+ // Looks like the string contains HTML markup
2286
+ const vnodes = [];
2287
+ let i = 0;
2288
+ let tagMatch = null;
2289
+ while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
2290
+ // Found what looks like an HTML tag
2291
+ // Calculate the index of the enclosed text within s
2292
+ const tag = tagMatch[0];
2293
+ let j = i + tagMatch.index + tag.length;
2294
+ // Find the end tag
2295
+ let end = s.indexOf("</" + tag.slice(1), j);
2296
+ if (end < 0) {
2297
+ // No end tag - skip past this weirdness
2298
+ i = j;
2299
+ continue;
2300
+ }
2301
+ // Add the text preceding the tag
2302
+ if (tagMatch.index > 0)
2303
+ vnodes.push(s.slice(i, i + tagMatch.index));
2304
+ // Create the Mithril node corresponding to the tag and the enclosed text
2305
+ // and add it to the list
2306
+ vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
2307
+ // Advance the index past the end of the tag
2308
+ i = end + tag.length + 1;
2309
+ }
2310
+ // Push the final text part, if any
2311
+ if (i < s.length)
2312
+ vnodes.push(s.slice(i));
2313
+ // Reassign s to the list of vnodes
2314
+ s = vnodes;
2315
+ }
2316
+ messages[key][lc] = s;
2317
+ }
2318
+ }
2319
+ messagesLoaded = true;
2320
+ }
2321
+ async function loadMessages(state, locale) {
2322
+ // Load the internationalization message JSON file from the server
2323
+ // and set the user's locale
2324
+ try {
2325
+ const messages = await requestWithoutAuth(state, {
2326
+ method: "GET",
2327
+ url: "/static/assets/messages.json",
2328
+ withCredentials: false, // Cookies are not allowed for CORS request
2329
+ });
2330
+ setLocale(locale, messages);
2331
+ }
2332
+ catch (_a) {
2333
+ setLocale(locale, {});
2334
+ }
2335
+ }
2336
+ function t(key, ips = {}) {
2337
+ // Main text translation function, supporting interpolation
2338
+ // and HTML tag substitution
2339
+ const msgDict = messages[key];
2340
+ if (msgDict === undefined)
2341
+ // No dictionary for this key - may actually be a missing entry
2342
+ return messagesLoaded ? key : "";
2343
+ // Lookup exact locale, then fallback, then resort to returning the key
2344
+ const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
2345
+ // If we have an interpolation object, do the interpolation first
2346
+ return Object.keys(ips).length ? interpolate(message, ips) : message;
2347
+ }
2348
+ function ts(key, ips = {}) {
2349
+ // String translation function, supporting interpolation
2350
+ // but not HTML tag substitution
2351
+ const msgDict = messages[key];
2352
+ if (msgDict === undefined)
2353
+ // No dictionary for this key - may actually be a missing entry
2354
+ return messagesLoaded ? key : "";
2355
+ // Lookup exact locale, then fallback, then resort to returning the key
2356
+ const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
2357
+ if (typeof message != "string")
2358
+ // This is actually an error - the client should be calling t() instead
2359
+ return "";
2360
+ // If we have an interpolation object, do the interpolation first
2361
+ return Object.keys(ips).length ? interpolate_string(message, ips) : message;
2362
+ }
2363
+ function mt(cls, children) {
2364
+ // Wrapper for the Mithril m() function that auto-translates
2365
+ // string and array arguments
2366
+ if (typeof children == "string") {
2367
+ return m(cls, t(children));
2368
+ }
2369
+ if (Array.isArray(children)) {
2370
+ return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
2371
+ }
2372
+ return m(cls, children);
2373
+ }
2374
+ function interpolate(message, ips) {
2375
+ // Replace interpolation placeholders with their corresponding values
2376
+ if (typeof message == "string") {
2377
+ return message.replace(rex, (match, key) => ips[key] || match);
2378
+ }
2379
+ if (Array.isArray(message)) {
2380
+ return message.map((item) => interpolate(item, ips));
2381
+ }
2382
+ return message;
2383
+ }
2384
+ function interpolate_string(message, ips) {
2385
+ // Replace interpolation placeholders with their corresponding values
2386
+ return message.replace(rex, (match, key) => ips[key] || match);
2387
+ }
2388
+
2389
+ /*
2390
+
2391
+ Types.ts
2392
+
2393
+ Common type definitions for the Explo/Netskrafl user interface
2394
+
2395
+ Copyright (C) 2025 Miðeind ehf.
2396
+ Author: Vilhjalmur Thorsteinsson
2397
+
2398
+ The Creative Commons Attribution-NonCommercial 4.0
2399
+ International Public License (CC-BY-NC 4.0) applies to this software.
2400
+ For further information, see https://github.com/mideind/Netskrafl
2401
+
2402
+ */
2403
+ // Global constants
2404
+ const RACK_SIZE = 7;
2405
+ const ROWIDS = "ABCDEFGHIJKLMNO";
2406
+ const BOARD_SIZE = ROWIDS.length;
2407
+ const EXTRA_WIDE_LETTERS = "q";
2408
+ const WIDE_LETTERS = "zxmæ";
2409
+ const ZOOM_FACTOR = 1.5;
2410
+ const ERROR_MESSAGES = {
2411
+ // Translations are found in /static/assets/messages.json
2412
+ 1: "Enginn stafur lagður niður",
2413
+ 2: "Fyrsta orð verður að liggja um byrjunarreitinn",
2414
+ 3: "Orð verður að vera samfellt á borðinu",
2415
+ 4: "Orð verður að tengjast orði sem fyrir er",
2416
+ 5: "Reitur þegar upptekinn",
2417
+ 6: "Ekki má vera eyða í orði",
2418
+ 7: "word_not_found",
2419
+ 8: "word_not_found",
2420
+ 9: "Of margir stafir lagðir niður",
2421
+ 10: "Stafur er ekki í rekkanum",
2422
+ 11: "Of fáir stafir eftir, skipting ekki leyfð",
2423
+ 12: "Of mörgum stöfum skipt",
2424
+ 13: "Leik vantar á borðið - notið F5/Refresh",
2425
+ 14: "Notandi ekki innskráður - notið F5/Refresh",
2426
+ 15: "Rangur eða óþekktur notandi",
2427
+ 16: "Viðureign finnst ekki",
2428
+ 17: "Viðureign er ekki utan tímamarka",
2429
+ 18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
2430
+ 19: "Véfenging er ekki möguleg í þessari viðureign",
2431
+ 20: "Síðasti leikur er ekki véfengjanlegur",
2432
+ 21: "Aðeins véfenging eða pass leyfileg",
2433
+ "server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
2234
2434
  };
2235
2435
 
2436
+ /*
2437
+
2438
+ Util.ts
2439
+
2440
+ Utility functions for the Explo/Netskrafl user interface
2441
+
2442
+ Copyright (C) 2025 Miðeind ehf.
2443
+ Author: Vilhjálmur Þorsteinsson
2444
+
2445
+ The Creative Commons Attribution-NonCommercial 4.0
2446
+ International Public License (CC-BY-NC 4.0) applies to this software.
2447
+ For further information, see https://github.com/mideind/Netskrafl
2448
+
2449
+ The following code is based on
2450
+ https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
2451
+
2452
+ */
2453
+ // Global vars to cache event state
2454
+ var evCache = [];
2455
+ var origDistance = -1;
2456
+ const PINCH_THRESHOLD = 10; // Minimum pinch movement
2457
+ var hasZoomed = false;
2458
+ // Old-style (non-single-page) game URL prefix
2459
+ const BOARD_PREFIX = "/board?game=";
2460
+ const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
2461
+ function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
2462
+ // Install event handlers for the pointer target
2463
+ attrs.onpointerdown = pointerdown_handler;
2464
+ attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
2465
+ // Use same handler for pointer{up,cancel,out,leave} events since
2466
+ // the semantics for these events - in this app - are the same.
2467
+ attrs.onpointerup = pointerup_handler;
2468
+ attrs.onpointercancel = pointerup_handler;
2469
+ attrs.onpointerout = pointerup_handler;
2470
+ attrs.onpointerleave = pointerup_handler;
2471
+ }
2472
+ function pointerdown_handler(ev) {
2473
+ // The pointerdown event signals the start of a touch interaction.
2474
+ // This event is cached to support 2-finger gestures
2475
+ evCache.push(ev);
2476
+ }
2477
+ function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
2478
+ // This function implements a 2-pointer horizontal pinch/zoom gesture.
2479
+ //
2480
+ // If the distance between the two pointers has increased (zoom in),
2481
+ // the target element's background is changed to "pink" and if the
2482
+ // distance is decreasing (zoom out), the color is changed to "lightblue".
2483
+ //
2484
+ // Find this event in the cache and update its record with this event
2485
+ for (let i = 0; i < evCache.length; i++) {
2486
+ if (ev.pointerId === evCache[i].pointerId) {
2487
+ evCache[i] = ev;
2488
+ break;
2489
+ }
2490
+ }
2491
+ // If two pointers are down, check for pinch gestures
2492
+ if (evCache.length == 2) {
2493
+ // Calculate the distance between the two pointers
2494
+ const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
2495
+ Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
2496
+ if (origDistance > 0) {
2497
+ if (curDistance - origDistance >= PINCH_THRESHOLD) {
2498
+ // The distance between the two pointers has increased
2499
+ if (!hasZoomed)
2500
+ funcZoomIn();
2501
+ hasZoomed = true;
2502
+ }
2503
+ else if (origDistance - curDistance >= PINCH_THRESHOLD) {
2504
+ // The distance between the two pointers has decreased
2505
+ if (!hasZoomed)
2506
+ funcZoomOut();
2507
+ hasZoomed = true;
2508
+ }
2509
+ }
2510
+ else if (origDistance < 0) {
2511
+ // Note the original difference between two pointers
2512
+ origDistance = curDistance;
2513
+ hasZoomed = false;
2514
+ }
2515
+ }
2516
+ }
2517
+ function pointerup_handler(ev) {
2518
+ // Remove this pointer from the cache and reset the target's
2519
+ // background and border
2520
+ remove_event(ev);
2521
+ // If the number of pointers down is less than two then reset diff tracker
2522
+ if (evCache.length < 2) {
2523
+ origDistance = -1;
2524
+ }
2525
+ }
2526
+ function remove_event(ev) {
2527
+ // Remove this event from the target's cache
2528
+ for (let i = 0; i < evCache.length; i++) {
2529
+ if (evCache[i].pointerId === ev.pointerId) {
2530
+ evCache.splice(i, 1);
2531
+ break;
2532
+ }
2533
+ }
2534
+ }
2535
+ function buttonOver(ev) {
2536
+ const clist = ev.currentTarget.classList;
2537
+ if (clist !== undefined && !clist.contains("disabled"))
2538
+ clist.add("over");
2539
+ ev.redraw = false;
2540
+ }
2541
+ function buttonOut(ev) {
2542
+ const clist = ev.currentTarget.classList;
2543
+ if (clist !== undefined)
2544
+ clist.remove("over");
2545
+ ev.redraw = false;
2546
+ }
2547
+ // Glyphicon utility function: inserts a glyphicon span
2548
+ function glyph(icon, attrs, grayed) {
2549
+ return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
2550
+ }
2551
+ function glyphGrayed(icon, attrs) {
2552
+ return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
2553
+ }
2554
+ // Utility function: inserts non-breaking space
2555
+ function nbsp(n) {
2556
+ return m.trust("&nbsp;");
2557
+ }
2558
+ // Utility functions
2559
+ function escapeHtml(string) {
2560
+ /* Utility function to properly encode a string into HTML */
2561
+ const entityMap = {
2562
+ "&": "&amp;",
2563
+ "<": "&lt;",
2564
+ ">": "&gt;",
2565
+ '"': '&quot;',
2566
+ "'": '&#39;',
2567
+ "/": '&#x2F;'
2568
+ };
2569
+ return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
2570
+ }
2571
+ function getUrlVars(url) {
2572
+ // Get values from a URL query string
2573
+ const hashes = url.split('&');
2574
+ const vars = {};
2575
+ for (let i = 0; i < hashes.length; i++) {
2576
+ const hash = hashes[i].split('=');
2577
+ if (hash.length == 2)
2578
+ vars[hash[0]] = decodeURIComponent(hash[1]);
2579
+ }
2580
+ return vars;
2581
+ }
2582
+ function getInput(id) {
2583
+ // Return the current value of a text input field
2584
+ const elem = document.getElementById(id);
2585
+ return elem.value;
2586
+ }
2587
+ function setInput(id, val) {
2588
+ // Set the current value of a text input field
2589
+ const elem = document.getElementById(id);
2590
+ elem.value = val;
2591
+ }
2592
+ function playAudio(elemId) {
2593
+ // Play an audio file
2594
+ const sound = document.getElementById(elemId);
2595
+ if (sound)
2596
+ sound.play();
2597
+ }
2598
+ function arrayEqual(a, b) {
2599
+ // Return true if arrays a and b are equal
2600
+ if (a.length != b.length)
2601
+ return false;
2602
+ for (let i = 0; i < a.length; i++)
2603
+ if (a[i] != b[i])
2604
+ return false;
2605
+ return true;
2606
+ }
2607
+ function gameUrl(url) {
2608
+ // Convert old-style game URL to new-style single-page URL
2609
+ // The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
2610
+ if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
2611
+ // Cut off "/board?game="
2612
+ url = url.slice(BOARD_PREFIX_LEN);
2613
+ // Isolate the game UUID
2614
+ const uuid = url.slice(0, 36);
2615
+ // Isolate the other parameters, if any
2616
+ let params = url.slice(36);
2617
+ // Start parameter section of URL with a ? sign
2618
+ if (params.length > 0 && params.charAt(0) == "&")
2619
+ params = "?" + params.slice(1);
2620
+ // Return the single-page URL, to be consumed by m.route.Link()
2621
+ return "/game/" + uuid + params;
2622
+ }
2623
+ function scrollMovelistToBottom() {
2624
+ // If the length of the move list has changed,
2625
+ // scroll the last move into view
2626
+ let movelist = document.querySelectorAll("div.movelist .move");
2627
+ if (!movelist || !movelist.length)
2628
+ return;
2629
+ let target = movelist[movelist.length - 1];
2630
+ let parent = target.parentNode;
2631
+ let len = parent.getAttribute("data-len");
2632
+ let intLen = (!len) ? 0 : parseInt(len);
2633
+ if (movelist.length > intLen) {
2634
+ // The list has grown since we last updated it:
2635
+ // scroll to the bottom and mark its length
2636
+ parent.scrollTop = target.offsetTop;
2637
+ }
2638
+ parent.setAttribute("data-len", movelist.length.toString());
2639
+ }
2640
+ function coord(row, col, vertical = false) {
2641
+ // Return the co-ordinate string for the given 0-based row and col
2642
+ if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
2643
+ return null;
2644
+ // Horizontal moves have the row letter first
2645
+ // Vertical moves have the column number first
2646
+ return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
2647
+ }
2648
+ function toVector(co) {
2649
+ // Convert a co-ordinate string to a 0-based row, col and direction vector
2650
+ var dx = 0, dy = 0;
2651
+ var col = 0;
2652
+ var row = ROWIDS.indexOf(co[0]);
2653
+ if (row >= 0) {
2654
+ /* Horizontal move */
2655
+ col = parseInt(co.slice(1)) - 1;
2656
+ dx = 1;
2657
+ }
2658
+ else {
2659
+ /* Vertical move */
2660
+ row = ROWIDS.indexOf(co.slice(-1));
2661
+ col = parseInt(co) - 1;
2662
+ dy = 1;
2663
+ }
2664
+ return { col: col, row: row, dx: dx, dy: dy };
2665
+ }
2666
+ function valueOrK(value, breakpoint = 10000) {
2667
+ // Return a numeric value as a string, but in kilos (thousands)
2668
+ // if it exceeds a breakpoint, in that case suffixed by "K"
2669
+ const sign = value < 0 ? "-" : "";
2670
+ value = Math.abs(value);
2671
+ if (value < breakpoint)
2672
+ return `${sign}${value}`;
2673
+ value = Math.round(value / 1000);
2674
+ return `${sign}${value}K`;
2675
+ }
2676
+ // SalesCloud stuff
2677
+ function doRegisterSalesCloud(i, s, o, g, r, a, m) {
2678
+ i.SalesCloudObject = r;
2679
+ i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
2680
+ i[r].l = 1 * new Date();
2681
+ a = s.createElement(o);
2682
+ m = s.getElementsByTagName(o)[0];
2683
+ a.src = g;
2684
+ m.parentNode.insertBefore(a, m);
2685
+ }
2686
+ function registerSalesCloud() {
2687
+ doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
2688
+ }
2689
+
2236
2690
  /**
2237
2691
  * @license
2238
2692
  * Copyright 2017 Google LLC
@@ -26558,33 +27012,46 @@ let database;
26558
27012
  let analytics;
26559
27013
  function initFirebase(state) {
26560
27014
  try {
26561
- const projectId = state.projectId;
27015
+ const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
26562
27016
  const firebaseOptions = {
26563
27017
  projectId,
26564
- apiKey: state.firebaseAPIKey,
27018
+ apiKey: firebaseAPIKey,
26565
27019
  authDomain: `${projectId}.firebaseapp.com`,
26566
- databaseURL: state.databaseURL,
27020
+ databaseURL,
26567
27021
  storageBucket: `${projectId}.firebasestorage.app`,
26568
- messagingSenderId: state.firebaseSenderId,
26569
- appId: state.firebaseAppId,
26570
- measurementId: state.measurementId,
27022
+ messagingSenderId: firebaseSenderId,
27023
+ appId: firebaseAppId,
27024
+ measurementId,
26571
27025
  };
26572
- app = initializeApp(firebaseOptions, "netskrafl");
27026
+ app = initializeApp(firebaseOptions, projectId);
26573
27027
  if (!app) {
26574
27028
  console.error("Failed to initialize Firebase");
27029
+ return false;
26575
27030
  }
26576
27031
  }
26577
27032
  catch (e) {
26578
27033
  console.error("Failed to initialize Firebase", e);
27034
+ return false;
27035
+ }
27036
+ return true;
27037
+ }
27038
+ function isFirebaseAuthenticated(state) {
27039
+ // Check if Firebase is currently authenticated
27040
+ // If auth is not initialized but state is provided, try to initialize
27041
+ if (!auth) {
27042
+ if (!app)
27043
+ initFirebase(state);
27044
+ if (app)
27045
+ auth = getAuth(app);
26579
27046
  }
27047
+ if (!auth)
27048
+ return false;
27049
+ return auth.currentUser !== null;
26580
27050
  }
26581
27051
  async function loginFirebase(state, firebaseToken, onLoginFunc) {
26582
- if (!app)
26583
- initFirebase(state);
26584
- if (!app)
27052
+ if (!app && !initFirebase(state))
26585
27053
  return;
26586
- const userId = state.userId;
26587
- const locale = state.locale;
27054
+ const { userId, locale, projectId, loginMethod } = state;
26588
27055
  auth = getAuth(app);
26589
27056
  if (!auth) {
26590
27057
  console.error("Failed to initialize Firebase Auth");
@@ -26597,16 +27064,16 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
26597
27064
  // For new users, log an additional signup event
26598
27065
  if (state.newUser) {
26599
27066
  logEvent("sign_up", {
26600
- locale: state.locale,
26601
- method: state.loginMethod,
26602
- userid: state.userId
27067
+ locale,
27068
+ method: loginMethod,
27069
+ userid: userId
26603
27070
  });
26604
27071
  }
26605
27072
  // And always log a login event
26606
27073
  logEvent("login", {
26607
- locale: state.locale,
26608
- method: state.loginMethod,
26609
- userid: state.userId
27074
+ locale,
27075
+ method: loginMethod,
27076
+ userid: userId
26610
27077
  });
26611
27078
  }
26612
27079
  });
@@ -26621,7 +27088,7 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
26621
27088
  if (!analytics) {
26622
27089
  console.error("Failed to initialize Firebase Analytics");
26623
27090
  }
26624
- initPresence(state.projectId, userId, locale);
27091
+ initPresence(projectId, userId, locale);
26625
27092
  }
26626
27093
  function initPresence(projectId, userId, locale) {
26627
27094
  // Ensure that this user connection is recorded in Firebase
@@ -26678,473 +27145,6 @@ function logEvent(ev, params) {
26678
27145
  logEvent$2(analytics, ev, params);
26679
27146
  }
26680
27147
 
26681
- /*
26682
-
26683
- i8n.ts
26684
-
26685
- Single page UI for Netskrafl/Explo using the Mithril library
26686
-
26687
- Copyright (C) 2025 Miðeind ehf.
26688
- Author: Vilhjálmur Þorsteinsson
26689
-
26690
- The Creative Commons Attribution-NonCommercial 4.0
26691
- International Public License (CC-BY-NC 4.0) applies to this software.
26692
- For further information, see https://github.com/mideind/Netskrafl
26693
-
26694
-
26695
- This module contains internationalization (i18n) utility functions,
26696
- allowing for translation of displayed text between languages.
26697
-
26698
- Text messages for individual locales are loaded from the
26699
- /static/assets/messages.json file, which is fetched from the server.
26700
-
26701
- */
26702
- // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
26703
- // This is overwritten in setLocale()
26704
- let currentLocale = "is_IS";
26705
- let currentFallback = "is";
26706
- // Regex that matches embedded interpolations such as "Welcome, {username}!"
26707
- // Interpolation identifiers should only contain ASCII characters, digits and '_'
26708
- const rex = /{\s*(\w+)\s*}/g;
26709
- let messages = {};
26710
- let messagesLoaded = false;
26711
- function hasAnyTranslation(msgs, locale) {
26712
- // Return true if any translation is available for the given locale
26713
- for (let key in msgs) {
26714
- if (msgs[key][locale] !== undefined)
26715
- return true;
26716
- }
26717
- return false;
26718
- }
26719
- function setLocale(locale, msgs) {
26720
- // Set the current i18n locale and fallback
26721
- currentLocale = locale;
26722
- currentFallback = locale.split("_")[0];
26723
- // For unsupported locales, i.e. locales that have no
26724
- // translations available for them, fall back to English (U.S.).
26725
- if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
26726
- currentLocale = "en_US";
26727
- currentFallback = "en";
26728
- }
26729
- // Flatten the Messages structure, enabling long strings
26730
- // to be represented as string arrays in the messages.json file
26731
- messages = {};
26732
- for (let key in msgs) {
26733
- for (let lc in msgs[key]) {
26734
- let s = msgs[key][lc];
26735
- if (Array.isArray(s))
26736
- s = s.join("");
26737
- if (messages[key] === undefined)
26738
- messages[key] = {};
26739
- // If the string s contains HTML markup of the form <tag>...</tag>,
26740
- // convert it into a list of Mithril Vnode children corresponding to
26741
- // the text and the tags
26742
- if (s.match(/<[a-z]+>/)) {
26743
- // Looks like the string contains HTML markup
26744
- const vnodes = [];
26745
- let i = 0;
26746
- let tagMatch = null;
26747
- while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
26748
- // Found what looks like an HTML tag
26749
- // Calculate the index of the enclosed text within s
26750
- const tag = tagMatch[0];
26751
- let j = i + tagMatch.index + tag.length;
26752
- // Find the end tag
26753
- let end = s.indexOf("</" + tag.slice(1), j);
26754
- if (end < 0) {
26755
- // No end tag - skip past this weirdness
26756
- i = j;
26757
- continue;
26758
- }
26759
- // Add the text preceding the tag
26760
- if (tagMatch.index > 0)
26761
- vnodes.push(s.slice(i, i + tagMatch.index));
26762
- // Create the Mithril node corresponding to the tag and the enclosed text
26763
- // and add it to the list
26764
- vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
26765
- // Advance the index past the end of the tag
26766
- i = end + tag.length + 1;
26767
- }
26768
- // Push the final text part, if any
26769
- if (i < s.length)
26770
- vnodes.push(s.slice(i));
26771
- // Reassign s to the list of vnodes
26772
- s = vnodes;
26773
- }
26774
- messages[key][lc] = s;
26775
- }
26776
- }
26777
- messagesLoaded = true;
26778
- }
26779
- async function loadMessages(locale) {
26780
- // Load the internationalization message JSON file from the server
26781
- // and set the user's locale
26782
- try {
26783
- const messages = await request({
26784
- method: "GET",
26785
- url: "/static/assets/messages.json",
26786
- withCredentials: false, // Cookies are not allowed for CORS request
26787
- });
26788
- setLocale(locale, messages);
26789
- }
26790
- catch (_a) {
26791
- setLocale(locale, {});
26792
- }
26793
- }
26794
- function t(key, ips = {}) {
26795
- // Main text translation function, supporting interpolation
26796
- // and HTML tag substitution
26797
- const msgDict = messages[key];
26798
- if (msgDict === undefined)
26799
- // No dictionary for this key - may actually be a missing entry
26800
- return messagesLoaded ? key : "";
26801
- // Lookup exact locale, then fallback, then resort to returning the key
26802
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
26803
- // If we have an interpolation object, do the interpolation first
26804
- return Object.keys(ips).length ? interpolate(message, ips) : message;
26805
- }
26806
- function ts(key, ips = {}) {
26807
- // String translation function, supporting interpolation
26808
- // but not HTML tag substitution
26809
- const msgDict = messages[key];
26810
- if (msgDict === undefined)
26811
- // No dictionary for this key - may actually be a missing entry
26812
- return messagesLoaded ? key : "";
26813
- // Lookup exact locale, then fallback, then resort to returning the key
26814
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
26815
- if (typeof message != "string")
26816
- // This is actually an error - the client should be calling t() instead
26817
- return "";
26818
- // If we have an interpolation object, do the interpolation first
26819
- return Object.keys(ips).length ? interpolate_string(message, ips) : message;
26820
- }
26821
- function mt(cls, children) {
26822
- // Wrapper for the Mithril m() function that auto-translates
26823
- // string and array arguments
26824
- if (typeof children == "string") {
26825
- return m(cls, t(children));
26826
- }
26827
- if (Array.isArray(children)) {
26828
- return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
26829
- }
26830
- return m(cls, children);
26831
- }
26832
- function interpolate(message, ips) {
26833
- // Replace interpolation placeholders with their corresponding values
26834
- if (typeof message == "string") {
26835
- return message.replace(rex, (match, key) => ips[key] || match);
26836
- }
26837
- if (Array.isArray(message)) {
26838
- return message.map((item) => interpolate(item, ips));
26839
- }
26840
- return message;
26841
- }
26842
- function interpolate_string(message, ips) {
26843
- // Replace interpolation placeholders with their corresponding values
26844
- return message.replace(rex, (match, key) => ips[key] || match);
26845
- }
26846
-
26847
- /*
26848
-
26849
- Types.ts
26850
-
26851
- Common type definitions for the Explo/Netskrafl user interface
26852
-
26853
- Copyright (C) 2025 Miðeind ehf.
26854
- Author: Vilhjalmur Thorsteinsson
26855
-
26856
- The Creative Commons Attribution-NonCommercial 4.0
26857
- International Public License (CC-BY-NC 4.0) applies to this software.
26858
- For further information, see https://github.com/mideind/Netskrafl
26859
-
26860
- */
26861
- // Global constants
26862
- const RACK_SIZE = 7;
26863
- const ROWIDS = "ABCDEFGHIJKLMNO";
26864
- const BOARD_SIZE = ROWIDS.length;
26865
- const EXTRA_WIDE_LETTERS = "q";
26866
- const WIDE_LETTERS = "zxmæ";
26867
- const ZOOM_FACTOR = 1.5;
26868
- const ERROR_MESSAGES = {
26869
- // Translations are found in /static/assets/messages.json
26870
- 1: "Enginn stafur lagður niður",
26871
- 2: "Fyrsta orð verður að liggja um byrjunarreitinn",
26872
- 3: "Orð verður að vera samfellt á borðinu",
26873
- 4: "Orð verður að tengjast orði sem fyrir er",
26874
- 5: "Reitur þegar upptekinn",
26875
- 6: "Ekki má vera eyða í orði",
26876
- 7: "word_not_found",
26877
- 8: "word_not_found",
26878
- 9: "Of margir stafir lagðir niður",
26879
- 10: "Stafur er ekki í rekkanum",
26880
- 11: "Of fáir stafir eftir, skipting ekki leyfð",
26881
- 12: "Of mörgum stöfum skipt",
26882
- 13: "Leik vantar á borðið - notið F5/Refresh",
26883
- 14: "Notandi ekki innskráður - notið F5/Refresh",
26884
- 15: "Rangur eða óþekktur notandi",
26885
- 16: "Viðureign finnst ekki",
26886
- 17: "Viðureign er ekki utan tímamarka",
26887
- 18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
26888
- 19: "Véfenging er ekki möguleg í þessari viðureign",
26889
- 20: "Síðasti leikur er ekki véfengjanlegur",
26890
- 21: "Aðeins véfenging eða pass leyfileg",
26891
- "server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
26892
- };
26893
-
26894
- /*
26895
-
26896
- Util.ts
26897
-
26898
- Utility functions for the Explo/Netskrafl user interface
26899
-
26900
- Copyright (C) 2025 Miðeind ehf.
26901
- Author: Vilhjálmur Þorsteinsson
26902
-
26903
- The Creative Commons Attribution-NonCommercial 4.0
26904
- International Public License (CC-BY-NC 4.0) applies to this software.
26905
- For further information, see https://github.com/mideind/Netskrafl
26906
-
26907
- The following code is based on
26908
- https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
26909
-
26910
- */
26911
- // Global vars to cache event state
26912
- var evCache = [];
26913
- var origDistance = -1;
26914
- const PINCH_THRESHOLD = 10; // Minimum pinch movement
26915
- var hasZoomed = false;
26916
- // Old-style (non-single-page) game URL prefix
26917
- const BOARD_PREFIX = "/board?game=";
26918
- const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
26919
- function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
26920
- // Install event handlers for the pointer target
26921
- attrs.onpointerdown = pointerdown_handler;
26922
- attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
26923
- // Use same handler for pointer{up,cancel,out,leave} events since
26924
- // the semantics for these events - in this app - are the same.
26925
- attrs.onpointerup = pointerup_handler;
26926
- attrs.onpointercancel = pointerup_handler;
26927
- attrs.onpointerout = pointerup_handler;
26928
- attrs.onpointerleave = pointerup_handler;
26929
- }
26930
- function pointerdown_handler(ev) {
26931
- // The pointerdown event signals the start of a touch interaction.
26932
- // This event is cached to support 2-finger gestures
26933
- evCache.push(ev);
26934
- }
26935
- function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
26936
- // This function implements a 2-pointer horizontal pinch/zoom gesture.
26937
- //
26938
- // If the distance between the two pointers has increased (zoom in),
26939
- // the target element's background is changed to "pink" and if the
26940
- // distance is decreasing (zoom out), the color is changed to "lightblue".
26941
- //
26942
- // Find this event in the cache and update its record with this event
26943
- for (let i = 0; i < evCache.length; i++) {
26944
- if (ev.pointerId === evCache[i].pointerId) {
26945
- evCache[i] = ev;
26946
- break;
26947
- }
26948
- }
26949
- // If two pointers are down, check for pinch gestures
26950
- if (evCache.length == 2) {
26951
- // Calculate the distance between the two pointers
26952
- const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
26953
- Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
26954
- if (origDistance > 0) {
26955
- if (curDistance - origDistance >= PINCH_THRESHOLD) {
26956
- // The distance between the two pointers has increased
26957
- if (!hasZoomed)
26958
- funcZoomIn();
26959
- hasZoomed = true;
26960
- }
26961
- else if (origDistance - curDistance >= PINCH_THRESHOLD) {
26962
- // The distance between the two pointers has decreased
26963
- if (!hasZoomed)
26964
- funcZoomOut();
26965
- hasZoomed = true;
26966
- }
26967
- }
26968
- else if (origDistance < 0) {
26969
- // Note the original difference between two pointers
26970
- origDistance = curDistance;
26971
- hasZoomed = false;
26972
- }
26973
- }
26974
- }
26975
- function pointerup_handler(ev) {
26976
- // Remove this pointer from the cache and reset the target's
26977
- // background and border
26978
- remove_event(ev);
26979
- // If the number of pointers down is less than two then reset diff tracker
26980
- if (evCache.length < 2) {
26981
- origDistance = -1;
26982
- }
26983
- }
26984
- function remove_event(ev) {
26985
- // Remove this event from the target's cache
26986
- for (let i = 0; i < evCache.length; i++) {
26987
- if (evCache[i].pointerId === ev.pointerId) {
26988
- evCache.splice(i, 1);
26989
- break;
26990
- }
26991
- }
26992
- }
26993
- function buttonOver(ev) {
26994
- const clist = ev.currentTarget.classList;
26995
- if (clist !== undefined && !clist.contains("disabled"))
26996
- clist.add("over");
26997
- ev.redraw = false;
26998
- }
26999
- function buttonOut(ev) {
27000
- const clist = ev.currentTarget.classList;
27001
- if (clist !== undefined)
27002
- clist.remove("over");
27003
- ev.redraw = false;
27004
- }
27005
- // Glyphicon utility function: inserts a glyphicon span
27006
- function glyph(icon, attrs, grayed) {
27007
- return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
27008
- }
27009
- function glyphGrayed(icon, attrs) {
27010
- return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
27011
- }
27012
- // Utility function: inserts non-breaking space
27013
- function nbsp(n) {
27014
- return m.trust("&nbsp;");
27015
- }
27016
- // Utility functions
27017
- function escapeHtml(string) {
27018
- /* Utility function to properly encode a string into HTML */
27019
- const entityMap = {
27020
- "&": "&amp;",
27021
- "<": "&lt;",
27022
- ">": "&gt;",
27023
- '"': '&quot;',
27024
- "'": '&#39;',
27025
- "/": '&#x2F;'
27026
- };
27027
- return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
27028
- }
27029
- function getUrlVars(url) {
27030
- // Get values from a URL query string
27031
- const hashes = url.split('&');
27032
- const vars = {};
27033
- for (let i = 0; i < hashes.length; i++) {
27034
- const hash = hashes[i].split('=');
27035
- if (hash.length == 2)
27036
- vars[hash[0]] = decodeURIComponent(hash[1]);
27037
- }
27038
- return vars;
27039
- }
27040
- function getInput(id) {
27041
- // Return the current value of a text input field
27042
- const elem = document.getElementById(id);
27043
- return elem.value;
27044
- }
27045
- function setInput(id, val) {
27046
- // Set the current value of a text input field
27047
- const elem = document.getElementById(id);
27048
- elem.value = val;
27049
- }
27050
- function playAudio(elemId) {
27051
- // Play an audio file
27052
- const sound = document.getElementById(elemId);
27053
- if (sound)
27054
- sound.play();
27055
- }
27056
- function arrayEqual(a, b) {
27057
- // Return true if arrays a and b are equal
27058
- if (a.length != b.length)
27059
- return false;
27060
- for (let i = 0; i < a.length; i++)
27061
- if (a[i] != b[i])
27062
- return false;
27063
- return true;
27064
- }
27065
- function gameUrl(url) {
27066
- // Convert old-style game URL to new-style single-page URL
27067
- // The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
27068
- if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
27069
- // Cut off "/board?game="
27070
- url = url.slice(BOARD_PREFIX_LEN);
27071
- // Isolate the game UUID
27072
- const uuid = url.slice(0, 36);
27073
- // Isolate the other parameters, if any
27074
- let params = url.slice(36);
27075
- // Start parameter section of URL with a ? sign
27076
- if (params.length > 0 && params.charAt(0) == "&")
27077
- params = "?" + params.slice(1);
27078
- // Return the single-page URL, to be consumed by m.route.Link()
27079
- return "/game/" + uuid + params;
27080
- }
27081
- function scrollMovelistToBottom() {
27082
- // If the length of the move list has changed,
27083
- // scroll the last move into view
27084
- let movelist = document.querySelectorAll("div.movelist .move");
27085
- if (!movelist || !movelist.length)
27086
- return;
27087
- let target = movelist[movelist.length - 1];
27088
- let parent = target.parentNode;
27089
- let len = parent.getAttribute("data-len");
27090
- let intLen = (!len) ? 0 : parseInt(len);
27091
- if (movelist.length > intLen) {
27092
- // The list has grown since we last updated it:
27093
- // scroll to the bottom and mark its length
27094
- parent.scrollTop = target.offsetTop;
27095
- }
27096
- parent.setAttribute("data-len", movelist.length.toString());
27097
- }
27098
- function coord(row, col, vertical = false) {
27099
- // Return the co-ordinate string for the given 0-based row and col
27100
- if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
27101
- return null;
27102
- // Horizontal moves have the row letter first
27103
- // Vertical moves have the column number first
27104
- return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
27105
- }
27106
- function toVector(co) {
27107
- // Convert a co-ordinate string to a 0-based row, col and direction vector
27108
- var dx = 0, dy = 0;
27109
- var col = 0;
27110
- var row = ROWIDS.indexOf(co[0]);
27111
- if (row >= 0) {
27112
- /* Horizontal move */
27113
- col = parseInt(co.slice(1)) - 1;
27114
- dx = 1;
27115
- }
27116
- else {
27117
- /* Vertical move */
27118
- row = ROWIDS.indexOf(co.slice(-1));
27119
- col = parseInt(co) - 1;
27120
- dy = 1;
27121
- }
27122
- return { col: col, row: row, dx: dx, dy: dy };
27123
- }
27124
- function valueOrK(value, breakpoint = 10000) {
27125
- // Return a numeric value as a string, but in kilos (thousands)
27126
- // if it exceeds a breakpoint, in that case suffixed by "K"
27127
- const sign = value < 0 ? "-" : "";
27128
- value = Math.abs(value);
27129
- if (value < breakpoint)
27130
- return `${sign}${value}`;
27131
- value = Math.round(value / 1000);
27132
- return `${sign}${value}K`;
27133
- }
27134
- // SalesCloud stuff
27135
- function doRegisterSalesCloud(i, s, o, g, r, a, m) {
27136
- i.SalesCloudObject = r;
27137
- i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
27138
- i[r].l = 1 * new Date();
27139
- a = s.createElement(o);
27140
- m = s.getElementsByTagName(o)[0];
27141
- a.src = g;
27142
- m.parentNode.insertBefore(a, m);
27143
- }
27144
- function registerSalesCloud() {
27145
- doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
27146
- }
27147
-
27148
27148
  /*
27149
27149
 
27150
27150
  Logo.ts
@@ -27254,7 +27254,7 @@ const NetskraflLegend = (initialVnode) => {
27254
27254
  m.redraw();
27255
27255
  }
27256
27256
  return {
27257
- oncreate: () => {
27257
+ oninit: () => {
27258
27258
  if (msStepTime && ival === 0) {
27259
27259
  ival = setInterval(doStep, msStepTime);
27260
27260
  }
@@ -27309,10 +27309,197 @@ const AnimatedNetskraflLogo = (initialVnode) => {
27309
27309
  };
27310
27310
  };
27311
27311
 
27312
+ /*
27313
+
27314
+ Login.ts
27315
+
27316
+ Login UI for Metskrafl using the Mithril library
27317
+
27318
+ Copyright (C) 2025 Miðeind ehf.
27319
+ Author: Vilhjálmur Þorsteinsson
27320
+
27321
+ The Creative Commons Attribution-NonCommercial 4.0
27322
+ International Public License (CC-BY-NC 4.0) applies to this software.
27323
+ For further information, see https://github.com/mideind/Netskrafl
27324
+
27325
+ This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
27326
+ straightforward JavaScript single-page reactive UI library.
27327
+
27328
+ */
27329
+ const loginUserByEmail = async (state) => {
27330
+ // Call the /login_malstadur endpoint on the server
27331
+ // to log in the user with the given email and token.
27332
+ // The token is a standard HS256-encoded JWT with aud "netskrafl"
27333
+ // and iss typically "malstadur".
27334
+ const { userEmail, userNick, userFullname, token } = state;
27335
+ return requestWithoutAuth(state, {
27336
+ method: "POST",
27337
+ url: "/login_malstadur",
27338
+ body: { email: userEmail, nickname: userNick, fullname: userFullname, token }
27339
+ });
27340
+ };
27341
+ const LoginError = {
27342
+ view: (vnode) => {
27343
+ return m("div.error", {
27344
+ style: { visibility: "visible" }
27345
+ }, vnode.children);
27346
+ }
27347
+ };
27348
+ const LoginForm = (initialVnode) => {
27349
+ const loginUrl = initialVnode.attrs.loginUrl;
27350
+ let loginInProgress = false;
27351
+ function doLogin(ev) {
27352
+ loginInProgress = true;
27353
+ ev.preventDefault();
27354
+ window.location.href = loginUrl;
27355
+ }
27356
+ return {
27357
+ view: () => {
27358
+ return m.fragment({}, [
27359
+ // This is visible on large screens
27360
+ m("div.loginform-large", [
27361
+ m(NetskraflLogoOnly, {
27362
+ className: "login-logo",
27363
+ width: 200,
27364
+ }),
27365
+ m(NetskraflLegend, {
27366
+ className: "login-legend",
27367
+ width: 600,
27368
+ msStepTime: 0
27369
+ }),
27370
+ mt("div.welcome", "welcome_0"),
27371
+ mt("div.welcome", "welcome_1"),
27372
+ mt("div.welcome", "welcome_2"),
27373
+ m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
27374
+ t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
27375
+ ])
27376
+ ]),
27377
+ // This is visible on small screens
27378
+ m("div.loginform-small", [
27379
+ m(NetskraflLogoOnly, {
27380
+ className: "login-logo",
27381
+ width: 160,
27382
+ }),
27383
+ m(NetskraflLegend, {
27384
+ className: "login-legend",
27385
+ width: 650,
27386
+ msStepTime: 0
27387
+ }),
27388
+ m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
27389
+ ])
27390
+ ]);
27391
+ }
27392
+ };
27393
+ };
27394
+
27395
+ // Global state for authentication
27396
+ let authPromise = null;
27397
+ // Custom error class for authentication failures
27398
+ class AuthenticationError extends Error {
27399
+ constructor() {
27400
+ super("Authentication required");
27401
+ this.name = "AuthenticationError";
27402
+ }
27403
+ }
27404
+ // Internal function to ensure authentication
27405
+ const ensureAuthenticated = async (state) => {
27406
+ // If login is already in progress, wait for it to complete
27407
+ if (authPromise) {
27408
+ await authPromise;
27409
+ return;
27410
+ }
27411
+ // Start new login attempt (either forced by 401 or needed for Firebase)
27412
+ authPromise = loginUserByEmail(state);
27413
+ try {
27414
+ const result = await authPromise;
27415
+ if (result.status === "expired") {
27416
+ // Token has expired, notify the React component if callback is set
27417
+ state.tokenExpired && state.tokenExpired();
27418
+ throw new Error("Token expired");
27419
+ }
27420
+ else if (result.status !== "success") {
27421
+ throw new Error(`Authentication failed: ${result.message || result.status}`);
27422
+ }
27423
+ // Success: Log in to Firebase with the token passed from the server
27424
+ await loginFirebase(state, result.firebase_token);
27425
+ }
27426
+ finally {
27427
+ // Reset the promise so future 401s can trigger a new login
27428
+ authPromise = null;
27429
+ }
27430
+ };
27431
+ // Internal authenticated request function
27432
+ const authenticatedRequest = async (state, options, retries = 0) => {
27433
+ // Before making the request, check if Firebase needs authentication
27434
+ // This handles the case where the user returns with a valid backend cookie but expired Firebase auth
27435
+ if (!retries && !isFirebaseAuthenticated(state)) {
27436
+ // Firebase is not authenticated, ensure authentication before making the request
27437
+ await ensureAuthenticated(state);
27438
+ }
27439
+ try {
27440
+ // Make the actual request
27441
+ // console.log("Making authenticated request to", options.url);
27442
+ return await m.request({
27443
+ withCredentials: true,
27444
+ ...options,
27445
+ url: serverUrl(state, options.url),
27446
+ extract: (xhr) => {
27447
+ // Check for 401 Unauthorized
27448
+ if (xhr.status === 401) {
27449
+ // console.log("Received 401 Unauthorized, triggering re-authentication");
27450
+ throw new AuthenticationError();
27451
+ }
27452
+ // Handle empty responses
27453
+ if (!xhr.responseText) {
27454
+ return null;
27455
+ }
27456
+ // Parse JSON response
27457
+ try {
27458
+ return JSON.parse(xhr.responseText);
27459
+ }
27460
+ catch (e) {
27461
+ // If JSON parsing fails, return the raw text
27462
+ return xhr.responseText;
27463
+ }
27464
+ }
27465
+ });
27466
+ }
27467
+ catch (error) {
27468
+ if (error instanceof AuthenticationError && !retries) {
27469
+ // Backend returned 401, perform full authentication and retry (force=true)
27470
+ await ensureAuthenticated(state);
27471
+ // Retry the original request
27472
+ return authenticatedRequest(state, options, retries + 1);
27473
+ }
27474
+ // Re-throw other errors
27475
+ throw error;
27476
+ }
27477
+ };
27478
+ const request = (state, options) => {
27479
+ // Enhanced request with automatic authentication handling
27480
+ return authenticatedRequest(state, options);
27481
+ };
27482
+ const requestMoves = (state, options) => {
27483
+ // Call the moves service on the Google App Engine backend
27484
+ const url = `${state.movesUrl}${options.url}`;
27485
+ const headers = {
27486
+ "Content-Type": "application/json; charset=UTF-8",
27487
+ Authorization: `Bearer ${state.movesAccessKey}`,
27488
+ ...options === null || options === void 0 ? void 0 : options.headers,
27489
+ };
27490
+ return m.request({
27491
+ withCredentials: false,
27492
+ method: "POST",
27493
+ ...options,
27494
+ url,
27495
+ headers,
27496
+ });
27497
+ };
27498
+
27312
27499
  const SPINNER_INITIAL_DELAY = 800; // milliseconds
27313
27500
  const Spinner = {
27314
27501
  // Show a spinner wait box, after an initial delay
27315
- oncreate: (vnode) => {
27502
+ oninit: (vnode) => {
27316
27503
  vnode.state.show = false;
27317
27504
  vnode.state.ival = setTimeout(() => {
27318
27505
  vnode.state.show = true;
@@ -27425,11 +27612,12 @@ const OnlinePresence = (initialVnode) => {
27425
27612
  const askServer = attrs.online === undefined;
27426
27613
  const id = attrs.id;
27427
27614
  const userId = attrs.userId;
27615
+ const state = attrs.state;
27428
27616
  let loading = false;
27429
27617
  async function _update() {
27430
27618
  if (askServer && !loading) {
27431
27619
  loading = true;
27432
- const json = await request({
27620
+ const json = await request(state, {
27433
27621
  method: "POST",
27434
27622
  url: "/onlinecheck",
27435
27623
  body: { user: userId }
@@ -27439,7 +27627,7 @@ const OnlinePresence = (initialVnode) => {
27439
27627
  }
27440
27628
  }
27441
27629
  return {
27442
- oncreate: _update,
27630
+ oninit: _update,
27443
27631
  view: (vnode) => {
27444
27632
  var _a, _b;
27445
27633
  if (!askServer)
@@ -27705,6 +27893,7 @@ const WaitDialog = (initialVnode) => {
27705
27893
  const attrs = initialVnode.attrs;
27706
27894
  const view = attrs.view;
27707
27895
  const model = view.model;
27896
+ const state = model.state;
27708
27897
  const duration = attrs.duration;
27709
27898
  const oppId = attrs.oppId;
27710
27899
  const key = attrs.challengeKey;
@@ -27720,9 +27909,9 @@ const WaitDialog = (initialVnode) => {
27720
27909
  async function updateOnline() {
27721
27910
  // Initiate an online check on the opponent
27722
27911
  try {
27723
- if (!oppId || !key)
27912
+ if (!oppId || !key || !state)
27724
27913
  return;
27725
- const json = await request({
27914
+ const json = await request(state, {
27726
27915
  method: "POST",
27727
27916
  url: "/initwait",
27728
27917
  body: { opp: oppId, key }
@@ -27738,8 +27927,10 @@ const WaitDialog = (initialVnode) => {
27738
27927
  }
27739
27928
  async function cancelWait() {
27740
27929
  // Cancel a pending wait for a timed game
27930
+ if (!state)
27931
+ return;
27741
27932
  try {
27742
- await request({
27933
+ await request(state, {
27743
27934
  method: "POST",
27744
27935
  url: "/cancelwait",
27745
27936
  body: {
@@ -27773,11 +27964,13 @@ const WaitDialog = (initialVnode) => {
27773
27964
  return {
27774
27965
  oncreate,
27775
27966
  view: () => {
27967
+ if (!state)
27968
+ return null;
27776
27969
  return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
27777
27970
  m(".chall-hdr", m("table", m("tbody", m("tr", [
27778
27971
  m("td", m("h1.chall-icon", glyph("time"))),
27779
27972
  m("td.l-border", [
27780
- m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline }),
27973
+ m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
27781
27974
  m("h1", oppNick),
27782
27975
  m("h2", oppName)
27783
27976
  ])
@@ -27820,6 +28013,7 @@ const AcceptDialog = (initialVnode) => {
27820
28013
  // is linked up with her opponent and a new game is started
27821
28014
  const attrs = initialVnode.attrs;
27822
28015
  const view = attrs.view;
28016
+ const state = view.model.state;
27823
28017
  const oppId = attrs.oppId;
27824
28018
  const key = attrs.challengeKey;
27825
28019
  let oppNick = attrs.oppNick;
@@ -27827,11 +28021,11 @@ const AcceptDialog = (initialVnode) => {
27827
28021
  let loading = false;
27828
28022
  async function waitCheck() {
27829
28023
  // Initiate a wait status check on the opponent
27830
- if (loading)
28024
+ if (loading || !state)
27831
28025
  return; // Already checking
27832
28026
  loading = true;
27833
28027
  try {
27834
- const json = await request({
28028
+ const json = await request(state, {
27835
28029
  method: "POST",
27836
28030
  url: "/waitcheck",
27837
28031
  body: { user: oppId, key }
@@ -27854,7 +28048,7 @@ const AcceptDialog = (initialVnode) => {
27854
28048
  }
27855
28049
  }
27856
28050
  return {
27857
- oncreate: waitCheck,
28051
+ oninit: waitCheck,
27858
28052
  view: () => {
27859
28053
  return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
27860
28054
  m(".chall-hdr", m("table", m("tbody", m("tr", [
@@ -28039,87 +28233,6 @@ const FriendCancelConfirmDialog = (initialVnode) => {
28039
28233
  };
28040
28234
  };
28041
28235
 
28042
- /*
28043
-
28044
- Login.ts
28045
-
28046
- Login UI for Metskrafl using the Mithril library
28047
-
28048
- Copyright (C) 2025 Miðeind ehf.
28049
- Author: Vilhjálmur Þorsteinsson
28050
-
28051
- The Creative Commons Attribution-NonCommercial 4.0
28052
- International Public License (CC-BY-NC 4.0) applies to this software.
28053
- For further information, see https://github.com/mideind/Netskrafl
28054
-
28055
- This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
28056
- straightforward JavaScript single-page reactive UI library.
28057
-
28058
- */
28059
- const loginUserByEmail = async (email, nickname, fullname, token) => {
28060
- // Call the /login_malstadur endpoint on the server
28061
- // to log in the user with the given email and token.
28062
- // The token is a standard HS256-encoded JWT with aud "netskrafl"
28063
- // and iss typically "malstadur".
28064
- return request({
28065
- method: "POST",
28066
- url: "/login_malstadur",
28067
- body: { email, nickname, fullname, token }
28068
- });
28069
- };
28070
- const LoginError = {
28071
- view: (vnode) => {
28072
- var _a;
28073
- return m("div.error", { style: { visibility: "visible" } }, ((_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.message) || "Error logging in");
28074
- }
28075
- };
28076
- const LoginForm = (initialVnode) => {
28077
- const loginUrl = initialVnode.attrs.loginUrl;
28078
- let loginInProgress = false;
28079
- function doLogin(ev) {
28080
- loginInProgress = true;
28081
- ev.preventDefault();
28082
- window.location.href = loginUrl;
28083
- }
28084
- return {
28085
- view: () => {
28086
- return m.fragment({}, [
28087
- // This is visible on large screens
28088
- m("div.loginform-large", [
28089
- m(NetskraflLogoOnly, {
28090
- className: "login-logo",
28091
- width: 200,
28092
- }),
28093
- m(NetskraflLegend, {
28094
- className: "login-legend",
28095
- width: 600,
28096
- msStepTime: 0
28097
- }),
28098
- mt("div.welcome", "welcome_0"),
28099
- mt("div.welcome", "welcome_1"),
28100
- mt("div.welcome", "welcome_2"),
28101
- m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
28102
- t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
28103
- ])
28104
- ]),
28105
- // This is visible on small screens
28106
- m("div.loginform-small", [
28107
- m(NetskraflLogoOnly, {
28108
- className: "login-logo",
28109
- width: 160,
28110
- }),
28111
- m(NetskraflLegend, {
28112
- className: "login-legend",
28113
- width: 650,
28114
- msStepTime: 0
28115
- }),
28116
- m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
28117
- ])
28118
- ]);
28119
- }
28120
- };
28121
- };
28122
-
28123
28236
  /*
28124
28237
 
28125
28238
  ChallengeDialog.ts
@@ -28150,7 +28263,11 @@ const ChallengeDialog = () => {
28150
28263
  m(".chall-hdr", m("table", m("tbody", m("tr", [
28151
28264
  m("td", m("h1.chall-icon", glyph("hand-right"))),
28152
28265
  m("td.l-border", [
28153
- m(OnlinePresence, { id: "chall-online", userId: item.userid }),
28266
+ m(OnlinePresence, {
28267
+ id: "chall-online",
28268
+ userId: item.userid,
28269
+ state,
28270
+ }),
28154
28271
  m("h1", item.nick),
28155
28272
  m("h2", item.fullname)
28156
28273
  ])
@@ -29466,7 +29583,7 @@ const PromoDialog = (initialVnode) => {
29466
29583
  initFunc();
29467
29584
  }
29468
29585
  return {
29469
- oncreate: _fetchContent,
29586
+ oninit: _fetchContent,
29470
29587
  view: (vnode) => {
29471
29588
  let initFunc = vnode.attrs.initFunc;
29472
29589
  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", {
@@ -29538,7 +29655,7 @@ const UserInfoDialog = (initialVnode) => {
29538
29655
  }
29539
29656
  }
29540
29657
  return {
29541
- oncreate: (vnode) => {
29658
+ oninit: (vnode) => {
29542
29659
  _updateRecentList(vnode);
29543
29660
  _updateStats(vnode);
29544
29661
  },
@@ -30528,7 +30645,9 @@ const Board = (initialVnode) => {
30528
30645
  const scale = view.boardScale || 1.0;
30529
30646
  let attrs = {};
30530
30647
  // Add handlers for pinch zoom functionality
30531
- addPinchZoom(attrs, view.zoomIn, view.zoomOut);
30648
+ // Note: resist the temptation to pass zoomIn/zoomOut directly,
30649
+ // as that would not bind the 'this' pointer correctly
30650
+ addPinchZoom(attrs, () => view.zoomIn(), () => view.zoomOut());
30532
30651
  if (scale !== 1.0)
30533
30652
  attrs.style = `transform: scale(${scale})`;
30534
30653
  return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
@@ -30706,7 +30825,7 @@ const Chat = (initialVnode) => {
30706
30825
  const { view } = initialVnode.attrs;
30707
30826
  const model = view.model;
30708
30827
  const game = model.game;
30709
- model.state;
30828
+ const state = model.state;
30710
30829
  function decodeTimestamp(ts) {
30711
30830
  // Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
30712
30831
  return {
@@ -30763,7 +30882,7 @@ const Chat = (initialVnode) => {
30763
30882
  for (const emoticon of EMOTICONS)
30764
30883
  if (str.indexOf(emoticon.icon) >= 0) {
30765
30884
  // The string contains the emoticon: prepare to replace all occurrences
30766
- const imgUrl = serverUrl(emoticon.image);
30885
+ const imgUrl = serverUrl(state, emoticon.image);
30767
30886
  const img = `<img src='${imgUrl}' height='32' width='32'>`;
30768
30887
  // Re the following trick, see https://stackoverflow.com/questions/1144783/
30769
30888
  // replacing-all-occurrences-of-a-string-in-javascript
@@ -31914,13 +32033,13 @@ class View {
31914
32033
  views.push(this.vwLogin());
31915
32034
  break;
31916
32035
  case "loginerror":
31917
- views.push(m(LoginError));
32036
+ views.push(m(LoginError, { key: "login-error" }, t("Villa við innskráningu")));
31918
32037
  break;
31919
32038
  case "main":
31920
- views.push(m(Main, { view: this }));
32039
+ views.push(m(Main, { key: "main", view: this }));
31921
32040
  break;
31922
32041
  case "game":
31923
- views.push(m(GameView, { view: this }));
32042
+ views.push(m(GameView, { key: "game", view: this }));
31924
32043
  break;
31925
32044
  case "review":
31926
32045
  const n = vwReview(this);
@@ -31928,7 +32047,7 @@ class View {
31928
32047
  break;
31929
32048
  case "thanks":
31930
32049
  // Display a thank-you dialog on top of the normal main screen
31931
- views.push(m(Main, { view: this }));
32050
+ views.push(m(Main, { key: "main", view: this }));
31932
32051
  // Be careful to add the Thanks dialog only once to the stack
31933
32052
  if (!this.dialogStack.length)
31934
32053
  this.showThanks();
@@ -31939,7 +32058,7 @@ class View {
31939
32058
  views.push(this.vwHelp(parseInt(m.route.param("tab") || ""), parseInt(m.route.param("faq") || "")));
31940
32059
  break;
31941
32060
  default:
31942
- return [m("div", t("Þessi vefslóð er ekki rétt"))];
32061
+ return [m("div", { key: "error" }, t("Þessi vefslóð er ekki rétt"))];
31943
32062
  }
31944
32063
  // Push any open dialogs
31945
32064
  for (const dialog of this.dialogStack) {
@@ -31954,7 +32073,7 @@ class View {
31954
32073
  }
31955
32074
  // Overlay a spinner, if active
31956
32075
  if (model.spinners)
31957
- views.push(m(Spinner));
32076
+ views.push(m(Spinner, { key: "spinner" }));
31958
32077
  return views;
31959
32078
  }
31960
32079
  // Dialog support
@@ -32035,7 +32154,7 @@ class View {
32035
32154
  zoomOut() {
32036
32155
  if (this.boardScale !== 1.0) {
32037
32156
  this.boardScale = 1.0;
32038
- setTimeout(this.resetScale);
32157
+ setTimeout(() => this.resetScale());
32039
32158
  }
32040
32159
  }
32041
32160
  resetScale() {
@@ -32082,7 +32201,7 @@ class View {
32082
32201
  // No game or we're in full screen mode: always 100% scale
32083
32202
  // Also, as soon as a move is being processed by the server, we zoom out
32084
32203
  this.boardScale = 1.0; // Needs to be done before setTimeout() call
32085
- setTimeout(this.resetScale);
32204
+ setTimeout(() => this.resetScale());
32086
32205
  return;
32087
32206
  }
32088
32207
  const tp = game.tilesPlaced();
@@ -32176,7 +32295,7 @@ class View {
32176
32295
  }
32177
32296
  }
32178
32297
  // Output literal HTML obtained from rawhelp.html on the server
32179
- return m.fragment({}, [
32298
+ return m.fragment({ key: "help" }, [
32180
32299
  m(LeftLogo),
32181
32300
  m(UserId, { view: this }),
32182
32301
  this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
@@ -32222,6 +32341,7 @@ class View {
32222
32341
  vnode.dom.querySelector("#nickname").focus();
32223
32342
  }
32224
32343
  return m(".modal-dialog", {
32344
+ key: "userprefs",
32225
32345
  id: "user-dialog",
32226
32346
  oncreate: initFocus
32227
32347
  // onupdate: initFocus
@@ -32303,11 +32423,12 @@ class View {
32303
32423
  model.loadUser(true); // Activate spinner while loading
32304
32424
  if (!model.user)
32305
32425
  // Nothing to edit (the spinner should be showing in this case)
32306
- return m.fragment({}, []);
32426
+ return m.fragment({ key: "userprefs-empty" }, []);
32307
32427
  return this.vwUserPrefsDialog();
32308
32428
  }
32309
32429
  vwUserInfo(args) {
32310
32430
  return m(UserInfoDialog, {
32431
+ key: "userinfodialog-" + args.userid,
32311
32432
  view: this,
32312
32433
  userid: args.userid,
32313
32434
  nick: args.nick,
@@ -32316,6 +32437,7 @@ class View {
32316
32437
  }
32317
32438
  vwPromo(args) {
32318
32439
  return m(PromoDialog, {
32440
+ key: "promo-" + args.kind,
32319
32441
  view: this,
32320
32442
  kind: args.kind,
32321
32443
  initFunc: args.initFunc
@@ -32323,6 +32445,7 @@ class View {
32323
32445
  }
32324
32446
  vwWait(args) {
32325
32447
  return m(WaitDialog, {
32448
+ key: "wait-" + args.challengeKey,
32326
32449
  view: this,
32327
32450
  oppId: args.oppId,
32328
32451
  oppNick: args.oppNick,
@@ -32333,6 +32456,7 @@ class View {
32333
32456
  }
32334
32457
  vwAccept(args) {
32335
32458
  return m(AcceptDialog, {
32459
+ key: "accept-" + args.challengeKey,
32336
32460
  view: this,
32337
32461
  oppId: args.oppId,
32338
32462
  oppNick: args.oppNick,
@@ -32343,7 +32467,7 @@ class View {
32343
32467
  var _a;
32344
32468
  const model = this.model;
32345
32469
  const loginUrl = ((_a = model.state) === null || _a === void 0 ? void 0 : _a.loginUrl) || "";
32346
- return m(LoginForm, { loginUrl });
32470
+ return m(LoginForm, { key: "login", loginUrl });
32347
32471
  }
32348
32472
  vwDialogs() {
32349
32473
  // Show prompt dialogs below game board, if any
@@ -32422,12 +32546,12 @@ class View {
32422
32546
  View.dialogViews = {
32423
32547
  userprefs: (view) => view.vwUserPrefs(),
32424
32548
  userinfo: (view, args) => view.vwUserInfo(args),
32425
- challenge: (view, args) => m(ChallengeDialog, { view, item: args }),
32549
+ challenge: (view, args) => m(ChallengeDialog, { key: "challenge-" + args.item.challenge_key, view, item: args }),
32426
32550
  promo: (view, args) => view.vwPromo(args),
32427
- friend: (view) => m(FriendPromoteDialog, { view }),
32428
- thanks: (view) => m(FriendThanksDialog, { view }),
32429
- cancel: (view) => m(FriendCancelDialog, { view }),
32430
- confirm: (view) => m(FriendCancelConfirmDialog, { view }),
32551
+ friend: (view) => m(FriendPromoteDialog, { key: "friend", view }),
32552
+ thanks: (view) => m(FriendThanksDialog, { key: "thanks", view }),
32553
+ cancel: (view) => m(FriendCancelDialog, { key: "cancel", view }),
32554
+ confirm: (view) => m(FriendCancelConfirmDialog, { key: "confirm", view }),
32431
32555
  wait: (view, args) => view.vwWait(args),
32432
32556
  accept: (view, args) => view.vwAccept(args)
32433
32557
  };
@@ -32465,7 +32589,7 @@ class WordChecker {
32465
32589
  }
32466
32590
  }
32467
32591
  }
32468
- async checkWords(locale, words) {
32592
+ async checkWords(state, locale, words) {
32469
32593
  // Return true if all words are valid in the given locale,
32470
32594
  // or false otherwise. Lookups are cached for efficiency.
32471
32595
  let cache = this.wordCheckCache[locale];
@@ -32493,7 +32617,7 @@ class WordChecker {
32493
32617
  }
32494
32618
  // We need a server roundtrip
32495
32619
  try {
32496
- const response = await requestMoves({
32620
+ const response = await requestMoves(state, {
32497
32621
  url: "/wordcheck",
32498
32622
  body: {
32499
32623
  locale,
@@ -32723,7 +32847,7 @@ const BOARD = {
32723
32847
  }
32724
32848
  };
32725
32849
  class BaseGame {
32726
- constructor(uuid, board_type = "standard") {
32850
+ constructor(uuid, state, board_type = "standard") {
32727
32851
  // Basic game properties that don't change while the game is underway
32728
32852
  this.locale = "is_IS";
32729
32853
  this.alphabet = "";
@@ -32757,6 +32881,7 @@ class BaseGame {
32757
32881
  this.board_type = board_type;
32758
32882
  this.startSquare = START_SQUARE[board_type];
32759
32883
  this.startCoord = START_COORD[board_type];
32884
+ this.state = state;
32760
32885
  }
32761
32886
  // Default init method that can be overridden
32762
32887
  init() {
@@ -32997,7 +33122,7 @@ class BaseGame {
32997
33122
  if (!this.manual) {
32998
33123
  // This is not a manual-wordcheck game:
32999
33124
  // Check the word that has been laid down
33000
- const found = await wordChecker.checkWords(this.locale, scoreResult.words);
33125
+ const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
33001
33126
  this.wordGood = found;
33002
33127
  this.wordBad = !found;
33003
33128
  }
@@ -33260,10 +33385,10 @@ const MAX_OVERTIME = 10 * 60.0;
33260
33385
  const DEBUG_OVERTIME = 1 * 60.0;
33261
33386
  const GAME_OVER = 99; // Error code corresponding to the Error class in skraflmechanics.py
33262
33387
  class Game extends BaseGame {
33263
- constructor(uuid, srvGame, moveListener, maxOvertime) {
33388
+ constructor(uuid, srvGame, moveListener, state, maxOvertime) {
33264
33389
  var _a;
33265
33390
  // Call parent constructor
33266
- super(uuid); // Default board_type: "standard"
33391
+ super(uuid, state); // Default board_type: "standard"
33267
33392
  // A class that represents a Netskrafl game instance on the client
33268
33393
  // Netskrafl-specific properties
33269
33394
  this.userid = ["", ""];
@@ -33520,7 +33645,7 @@ class Game extends BaseGame {
33520
33645
  try {
33521
33646
  if (!this.uuid)
33522
33647
  return;
33523
- const result = await request({
33648
+ const result = await request(this.state, {
33524
33649
  method: "POST",
33525
33650
  url: "/gamestate",
33526
33651
  body: { game: this.uuid } // !!! FIXME: Add delete_zombie parameter
@@ -33723,7 +33848,7 @@ class Game extends BaseGame {
33723
33848
  this.chatLoading = true;
33724
33849
  this.messages = [];
33725
33850
  try {
33726
- const result = await request({
33851
+ const result = await request(this.state, {
33727
33852
  method: "POST",
33728
33853
  url: "/chatload",
33729
33854
  body: { channel: "game:" + this.uuid }
@@ -33749,7 +33874,7 @@ class Game extends BaseGame {
33749
33874
  // Load statistics about a game
33750
33875
  this.stats = undefined; // Error/in-progress status
33751
33876
  try {
33752
- const json = await request({
33877
+ const json = await request(this.state, {
33753
33878
  method: "POST",
33754
33879
  url: "/gamestats",
33755
33880
  body: { game: this.uuid }
@@ -33769,7 +33894,7 @@ class Game extends BaseGame {
33769
33894
  async sendMessage(msg) {
33770
33895
  // Send a chat message
33771
33896
  try {
33772
- await request({
33897
+ await request(this.state, {
33773
33898
  method: "POST",
33774
33899
  url: "/chatmsg",
33775
33900
  body: { channel: "game:" + this.uuid, msg: msg }
@@ -33971,10 +34096,10 @@ class Game extends BaseGame {
33971
34096
  // Send a move to the server
33972
34097
  this.moveInProgress = true;
33973
34098
  try {
33974
- const result = await request({
34099
+ const result = await request(this.state, {
33975
34100
  method: "POST",
33976
34101
  url: "/submitmove",
33977
- body: { moves: moves, mcount: this.moves.length, uuid: this.uuid }
34102
+ body: { moves: moves, mcount: this.moves.length, uuid: this.uuid },
33978
34103
  });
33979
34104
  // The update() function also handles error results
33980
34105
  this.update(result);
@@ -34000,7 +34125,7 @@ class Game extends BaseGame {
34000
34125
  // Force resignation by a tardy opponent
34001
34126
  this.moveInProgress = true;
34002
34127
  try {
34003
- const result = await request({
34128
+ const result = await request(this.state, {
34004
34129
  method: "POST",
34005
34130
  url: "/forceresign",
34006
34131
  body: { mcount: this.moves.length, game: this.uuid }
@@ -34124,7 +34249,10 @@ const HOT_WARM_BOUNDARY_RATIO = 0.6;
34124
34249
  const WARM_COLD_BOUNDARY_RATIO = 0.3;
34125
34250
  class Riddle extends BaseGame {
34126
34251
  constructor(uuid, date, model) {
34127
- super(uuid);
34252
+ if (!model.state) {
34253
+ throw new Error("No global state in Riddle constructor");
34254
+ }
34255
+ super(uuid, model.state);
34128
34256
  // Scoring properties, static
34129
34257
  this.bestPossibleScore = 0;
34130
34258
  this.warmBoundary = 0;
@@ -34165,9 +34293,12 @@ class Riddle extends BaseGame {
34165
34293
  async load(date, locale) {
34166
34294
  this.date = date;
34167
34295
  this.locale = locale;
34296
+ const { state } = this;
34168
34297
  try {
34298
+ if (!state)
34299
+ throw new Error("No global state in Riddle.load");
34169
34300
  // Request riddle data from server (HTTP API call)
34170
- const response = await request({
34301
+ const response = await request(state, {
34171
34302
  method: "POST",
34172
34303
  url: "/gatadagsins/riddle",
34173
34304
  body: { date, locale }
@@ -34195,11 +34326,11 @@ class Riddle extends BaseGame {
34195
34326
  }
34196
34327
  }
34197
34328
  async submitRiddleWord(move) {
34198
- const { state } = this.model;
34329
+ const { state } = this;
34199
34330
  if (!state || !state.userId)
34200
34331
  return;
34201
34332
  try {
34202
- await request({
34333
+ await request(state, {
34203
34334
  method: "POST",
34204
34335
  url: "/gatadagsins/submit",
34205
34336
  body: {
@@ -34479,8 +34610,6 @@ function getSettings() {
34479
34610
  }
34480
34611
  class Model {
34481
34612
  constructor(settings, state) {
34482
- // A class for the underlying data model, displayed by the current view
34483
- this.state = null;
34484
34613
  this.paths = [];
34485
34614
  // The routeName will be "login", "main", "game"...
34486
34615
  this.routeName = undefined;
@@ -34539,7 +34668,54 @@ class Model {
34539
34668
  this.isExplo = state.isExplo;
34540
34669
  this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
34541
34670
  // Load localized text messages from the messages.json file
34542
- loadMessages(state.locale);
34671
+ loadMessages(state, state.locale);
34672
+ }
34673
+ // Simple POST request with JSON body (most common case)
34674
+ async post(url, body) {
34675
+ if (!this.state) {
34676
+ throw new Error("Model state is not initialized");
34677
+ }
34678
+ return request(this.state, {
34679
+ method: "POST",
34680
+ url,
34681
+ body
34682
+ });
34683
+ }
34684
+ // GET request for HTML/text content
34685
+ async getText(url) {
34686
+ if (!this.state) {
34687
+ throw new Error("Model state is not initialized");
34688
+ }
34689
+ return request(this.state, {
34690
+ method: "GET",
34691
+ url,
34692
+ responseType: "text",
34693
+ deserialize: (str) => str
34694
+ });
34695
+ }
34696
+ // POST request that returns text/HTML
34697
+ async postText(url, body) {
34698
+ if (!this.state) {
34699
+ throw new Error("Model state is not initialized");
34700
+ }
34701
+ return request(this.state, {
34702
+ method: "POST",
34703
+ url,
34704
+ body,
34705
+ responseType: "text",
34706
+ deserialize: (str) => str
34707
+ });
34708
+ }
34709
+ // Request to the moves service
34710
+ async postMoves(body) {
34711
+ if (!this.state) {
34712
+ throw new Error("Model state is not initialized");
34713
+ }
34714
+ return requestMoves(this.state, {
34715
+ method: "POST",
34716
+ url: "/moves",
34717
+ body
34718
+ });
34543
34719
  }
34544
34720
  async loadGame(uuid, funcComplete, deleteZombie = false) {
34545
34721
  var _a;
@@ -34558,20 +34734,16 @@ class Model {
34558
34734
  this.highlightedMove = null;
34559
34735
  if (!uuid)
34560
34736
  return; // Should not happen
34561
- const result = await request({
34562
- method: "POST",
34563
- url: "/gamestate",
34564
- body: {
34565
- game: uuid,
34566
- delete_zombie: deleteZombie
34567
- }
34737
+ const result = await this.post("/gamestate", {
34738
+ game: uuid,
34739
+ delete_zombie: deleteZombie
34568
34740
  });
34569
34741
  if (!(result === null || result === void 0 ? void 0 : result.ok)) {
34570
34742
  // console.log("Game " + uuid + " could not be loaded");
34571
34743
  }
34572
34744
  else {
34573
34745
  // Create a new game instance and load the state into it
34574
- this.game = new Game(uuid, result.game, this, ((_a = this.state) === null || _a === void 0 ? void 0 : _a.runningLocal) ? DEBUG_OVERTIME : MAX_OVERTIME);
34746
+ 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);
34575
34747
  // Successfully loaded: call the completion function, if given
34576
34748
  // (this usually attaches the Firebase event listener)
34577
34749
  funcComplete && funcComplete();
@@ -34594,11 +34766,7 @@ class Model {
34594
34766
  this.numChallenges = 0;
34595
34767
  this.oppReady = 0;
34596
34768
  try {
34597
- const json = await request({
34598
- method: "POST",
34599
- url: "/allgamelists",
34600
- body: { zombie: includeZombies, count: 40 }
34601
- });
34769
+ const json = await this.post("/allgamelists", { zombie: includeZombies, count: 40 });
34602
34770
  if (!json || json.result !== 0) {
34603
34771
  // An error occurred
34604
34772
  this.gameList = [];
@@ -34640,11 +34808,7 @@ class Model {
34640
34808
  this.numGames = 0;
34641
34809
  this.spinners++;
34642
34810
  try {
34643
- const json = await request({
34644
- method: "POST",
34645
- url: "/gamelist",
34646
- body: { zombie: includeZombies }
34647
- });
34811
+ const json = await this.post("/gamelist", { zombie: includeZombies });
34648
34812
  if (!json || json.result !== 0) {
34649
34813
  // An error occurred
34650
34814
  this.gameList = [];
@@ -34674,10 +34838,7 @@ class Model {
34674
34838
  this.oppReady = 0;
34675
34839
  this.spinners++; // Show spinner while loading
34676
34840
  try {
34677
- const json = await request({
34678
- method: "POST",
34679
- url: "/challengelist"
34680
- });
34841
+ const json = await this.post("/challengelist");
34681
34842
  if (!json || json.result !== 0) {
34682
34843
  // An error occurred
34683
34844
  this.challengeList = [];
@@ -34711,11 +34872,7 @@ class Model {
34711
34872
  this.recentList = [];
34712
34873
  this.spinners++; // Show spinner while loading
34713
34874
  try {
34714
- const json = await request({
34715
- method: "POST",
34716
- url: "/recentlist",
34717
- body: { versus: null, count: 40 }
34718
- });
34875
+ const json = await this.post("/recentlist", { versus: null, count: 40 });
34719
34876
  if (!json || json.result !== 0) {
34720
34877
  // An error occurred
34721
34878
  this.recentList = [];
@@ -34734,11 +34891,7 @@ class Model {
34734
34891
  }
34735
34892
  async loadUserRecentList(userid, versus, readyFunc) {
34736
34893
  // Load the list of recent games for the given user
34737
- const json = await request({
34738
- method: "POST",
34739
- url: "/recentlist",
34740
- body: { user: userid, versus: versus, count: 40 }
34741
- });
34894
+ const json = await this.post("/recentlist", { user: userid, versus: versus, count: 40 });
34742
34895
  readyFunc(json);
34743
34896
  }
34744
34897
  async loadUserList(criteria) {
@@ -34756,11 +34909,7 @@ class Model {
34756
34909
  const url = "/userlist";
34757
34910
  const body = criteria;
34758
34911
  try {
34759
- const json = await request({
34760
- method: "POST",
34761
- url,
34762
- body,
34763
- });
34912
+ const json = await this.post(url, body);
34764
34913
  if (!json || json.result !== 0) {
34765
34914
  // An error occurred
34766
34915
  this.userList = [];
@@ -34782,11 +34931,7 @@ class Model {
34782
34931
  const url = "/rating";
34783
34932
  const body = { kind: spec };
34784
34933
  try {
34785
- const json = await request({
34786
- method: "POST",
34787
- url,
34788
- body,
34789
- });
34934
+ const json = await this.post(url, body);
34790
34935
  if (!json || json.result !== 0) {
34791
34936
  // An error occurred
34792
34937
  this.eloRatingList = [];
@@ -34805,11 +34950,7 @@ class Model {
34805
34950
  // Load statistics for the current user
34806
34951
  this.ownStats = {};
34807
34952
  try {
34808
- const json = await request({
34809
- method: "POST",
34810
- url: "/userstats",
34811
- body: {} // Current user is implicit
34812
- });
34953
+ const json = await this.post("/userstats", {});
34813
34954
  if (!json || json.result !== 0) {
34814
34955
  // An error occurred
34815
34956
  return;
@@ -34822,11 +34963,7 @@ class Model {
34822
34963
  async loadUserStats(userid, readyFunc) {
34823
34964
  // Load statistics for the given user
34824
34965
  try {
34825
- const json = await request({
34826
- method: "POST",
34827
- url: "/userstats",
34828
- body: { user: userid }
34829
- });
34966
+ const json = await this.post("/userstats", { user: userid });
34830
34967
  readyFunc(json);
34831
34968
  }
34832
34969
  catch (e) {
@@ -34836,13 +34973,7 @@ class Model {
34836
34973
  async loadPromoContent(key, readyFunc) {
34837
34974
  // Load HTML content for promo dialog
34838
34975
  try {
34839
- const html = await request({
34840
- method: "POST",
34841
- url: "/promo",
34842
- body: { key: key },
34843
- responseType: "text",
34844
- deserialize: (str) => str
34845
- });
34976
+ const html = await this.postText("/promo", { key: key });
34846
34977
  readyFunc(html);
34847
34978
  }
34848
34979
  catch (e) {
@@ -34891,11 +35022,7 @@ class Model {
34891
35022
  rack,
34892
35023
  limit: NUM_BEST_MOVES,
34893
35024
  };
34894
- const json = await requestMoves({
34895
- method: "POST",
34896
- url: "/moves",
34897
- body: rq,
34898
- });
35025
+ const json = await this.postMoves(rq);
34899
35026
  this.highlightedMove = null;
34900
35027
  if (!json || json.moves === undefined) {
34901
35028
  // Something unexpected going on
@@ -34930,12 +35057,7 @@ class Model {
34930
35057
  return; // Already loaded
34931
35058
  try {
34932
35059
  const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
34933
- const result = await request({
34934
- method: "GET",
34935
- url: "/rawhelp?version=malstadur&locale=" + locale,
34936
- responseType: "text",
34937
- deserialize: (str) => str
34938
- });
35060
+ const result = await this.getText("/rawhelp?version=malstadur&locale=" + locale);
34939
35061
  this.helpHTML = result;
34940
35062
  }
34941
35063
  catch (e) {
@@ -34950,12 +35072,7 @@ class Model {
34950
35072
  return; // Already loaded
34951
35073
  try {
34952
35074
  const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
34953
- const result = await request({
34954
- method: "GET",
34955
- url: "/friend?locale=" + locale,
34956
- responseType: "text",
34957
- deserialize: (str) => str
34958
- });
35075
+ const result = await this.getText("/friend?locale=" + locale);
34959
35076
  this.friendHTML = result;
34960
35077
  }
34961
35078
  catch (e) {
@@ -34971,10 +35088,7 @@ class Model {
34971
35088
  this.spinners++;
34972
35089
  }
34973
35090
  try {
34974
- const result = await request({
34975
- method: "POST",
34976
- url: "/loaduserprefs",
34977
- });
35091
+ const result = await this.post("/loaduserprefs");
34978
35092
  if (!result || !result.ok) {
34979
35093
  this.user = null;
34980
35094
  this.userErrors = null;
@@ -35001,11 +35115,7 @@ class Model {
35001
35115
  if (!user)
35002
35116
  return;
35003
35117
  try {
35004
- const result = await request({
35005
- method: "POST",
35006
- url: "/saveuserprefs",
35007
- body: user
35008
- });
35118
+ const result = await this.post("/saveuserprefs", user);
35009
35119
  if (result === null || result === void 0 ? void 0 : result.ok) {
35010
35120
  // User preferences modified successfully on the server:
35011
35121
  // update the state variables that we're caching
@@ -35419,8 +35529,10 @@ class Actions {
35419
35529
  }
35420
35530
  async markFavorite(userId, status) {
35421
35531
  // Mark or de-mark a user as a favorite
35532
+ if (!this.model.state)
35533
+ return;
35422
35534
  try {
35423
- await request({
35535
+ await request(this.model.state, {
35424
35536
  method: "POST",
35425
35537
  url: "/favorite",
35426
35538
  body: { destuser: userId, action: status ? "add" : "delete" }
@@ -35438,8 +35550,10 @@ class Actions {
35438
35550
  async handleChallenge(parameters) {
35439
35551
  var _a;
35440
35552
  // Reject or retract a challenge
35553
+ if (!this.model.state)
35554
+ return;
35441
35555
  try {
35442
- const json = await request({
35556
+ const json = await request(this.model.state, {
35443
35557
  method: "POST",
35444
35558
  url: "/challenge",
35445
35559
  body: parameters
@@ -35494,6 +35608,8 @@ class Actions {
35494
35608
  async startNewGame(oppid, reverse = false) {
35495
35609
  var _a;
35496
35610
  // Ask the server to initiate a new game against the given opponent
35611
+ if (!this.model.state)
35612
+ return;
35497
35613
  try {
35498
35614
  const rqBody = { opp: oppid, rev: reverse };
35499
35615
  if (this.model.isExplo) {
@@ -35506,7 +35622,7 @@ class Actions {
35506
35622
  url: "/initgame",
35507
35623
  body: rqBody
35508
35624
  };
35509
- const json = await request(rq);
35625
+ const json = await request(this.model.state, rq);
35510
35626
  if (json === null || json === void 0 ? void 0 : json.ok) {
35511
35627
  // Log the new game event
35512
35628
  const locale = ((_a = this.model.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
@@ -35572,8 +35688,10 @@ class Actions {
35572
35688
  // User Preference Management Actions
35573
35689
  async setUserPref(pref) {
35574
35690
  // Set a user preference on the server
35691
+ if (!this.model.state)
35692
+ return;
35575
35693
  try {
35576
- await request({
35694
+ await request(this.model.state, {
35577
35695
  method: "POST",
35578
35696
  url: "/setuserpref",
35579
35697
  body: pref
@@ -35590,7 +35708,7 @@ class Actions {
35590
35708
  if (!user || !state)
35591
35709
  return false;
35592
35710
  try {
35593
- const json = await request({
35711
+ const json = await request(state, {
35594
35712
  method: "POST",
35595
35713
  url: "/cancelplan",
35596
35714
  body: {}
@@ -35708,41 +35826,25 @@ async function main$1(state, container) {
35708
35826
  console.error("No container element found");
35709
35827
  return "error";
35710
35828
  }
35711
- // Set up Netskrafl backend server URLs
35712
- setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
35713
35829
  try {
35714
- const loginData = await loginUserByEmail(state.userEmail, state.userNick, state.userFullname, state.token);
35715
- if (loginData.status === "expired") {
35716
- // The current Málstaður JWT has expired;
35717
- // we need to obtain a new one
35718
- return "expired";
35719
- }
35720
- if (loginData.status === "success") {
35721
- state.userId = loginData.user_id;
35722
- // Use the nickname from the server, if available
35723
- state.userNick = loginData.nickname || state.userNick;
35724
- // Log in to Firebase with the token passed from the server
35725
- await loginFirebase(state, loginData.firebase_token);
35726
- // Everything looks OK:
35727
- // Create the model, actions and view objects in proper sequence
35728
- const settings = getSettings();
35729
- const model = new Model(settings, state);
35730
- const actions = new Actions(model);
35731
- const view = new View(actions);
35732
- // Run the Mithril router
35733
- const routeResolver = createRouteResolver(actions, view);
35734
- m.route(container, settings.defaultRoute, routeResolver);
35735
- return "success";
35736
- }
35830
+ // Skip initial login - authentication will happen lazily on first API call
35831
+ // Create the model, actions and view objects in proper sequence
35832
+ const settings = getSettings();
35833
+ const model = new Model(settings, state);
35834
+ const actions = new Actions(model);
35835
+ const view = new View(actions);
35836
+ // Run the Mithril router
35837
+ const routeResolver = createRouteResolver(actions, view);
35838
+ m.route(container, settings.defaultRoute, routeResolver);
35737
35839
  }
35738
35840
  catch (e) {
35739
- console.error("Exception during login: ", e);
35841
+ console.error("Exception during initialization: ", e);
35842
+ return "error";
35740
35843
  }
35741
- m.mount(container, LoginError);
35742
- return "error";
35844
+ return "success";
35743
35845
  }
35744
35846
 
35745
- const mountForUser$1 = async (state, tokenExpired) => {
35847
+ const mountForUser$1 = async (state) => {
35746
35848
  // Return a DOM tree containing a mounted Netskrafl UI
35747
35849
  // for the user specified in the state object
35748
35850
  const { userEmail } = state;
@@ -35770,18 +35872,12 @@ const mountForUser$1 = async (state, tokenExpired) => {
35770
35872
  root.className = "netskrafl-user";
35771
35873
  return root;
35772
35874
  }
35773
- else if (loginResult === "expired") {
35774
- // We need a new token from the Málstaður backend
35775
- root.className = "netskrafl-expired";
35776
- tokenExpired && tokenExpired(); // This causes a reload of the component
35777
- return root;
35778
- }
35779
35875
  // console.error("Failed to mount Netskrafl UI for user", userEmail);
35780
35876
  throw new Error("Failed to mount Netskrafl UI");
35781
35877
  };
35782
35878
  const NetskraflImpl = ({ state, tokenExpired }) => {
35783
35879
  const ref = React.createRef();
35784
- const completeState = { ...DEFAULT_STATE, ...state };
35880
+ const completeState = makeGlobalState({ ...state, tokenExpired });
35785
35881
  const { userEmail } = completeState;
35786
35882
  /*
35787
35883
  useEffect(() => {
@@ -35814,7 +35910,7 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
35814
35910
  return;
35815
35911
  }
35816
35912
  try {
35817
- mountForUser$1(completeState, tokenExpired).then((div) => {
35913
+ mountForUser$1(completeState).then((div) => {
35818
35914
  // Attach the div as a child of the container
35819
35915
  // instead of any previous children
35820
35916
  const container = ref.current;
@@ -35924,8 +36020,8 @@ const GataDagsinsBoardAndRack = {
35924
36020
  // If no riddle is available, return an empty div
35925
36021
  return m(".gatadagsins-board-rack-wrapper", "");
35926
36022
  }
35927
- // Is the move currently on the board the best possible move?
35928
- const celebrate = (riddle.currentScore || 0) >= riddle.bestPossibleScore;
36023
+ // Is the move currently on the board valid, and the best possible move?
36024
+ const celebrate = riddle.wordGood && (riddle.currentScore || 0) >= riddle.bestPossibleScore;
35929
36025
  return m(".gatadagsins-board-rack-wrapper", [
35930
36026
  // Board area (top)
35931
36027
  m(".gatadagsins-board-area" + (celebrate ? ".celebrate" : ""), [
@@ -36374,44 +36470,28 @@ async function main(state, container) {
36374
36470
  console.error("No container element found");
36375
36471
  return "error";
36376
36472
  }
36377
- // Set up Netskrafl backend server URLs
36378
- setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
36379
36473
  try {
36380
- const loginData = await loginUserByEmail(state.userEmail, state.userNick, state.userFullname, state.token);
36381
- if (loginData.status === "expired") {
36382
- // The current Málstaður JWT has expired;
36383
- // we need to obtain a new one
36384
- return "expired";
36385
- }
36386
- if (loginData.status === "success") {
36387
- state.userId = loginData.user_id;
36388
- // Use the nickname from the server, if available
36389
- state.userNick = loginData.nickname || state.userNick;
36390
- // Log in to Firebase with the token passed from the server
36391
- await loginFirebase(state, loginData.firebase_token);
36392
- // Everything looks OK:
36393
- // Create the model, view and actions objects
36394
- const settings = getSettings();
36395
- const model = new Model(settings, state);
36396
- const actions = new Actions(model);
36397
- const view = new View(actions);
36398
- const today = new Date().toISOString().split("T")[0];
36399
- const locale = state.locale || "is_IS";
36400
- // Mount the Gáta Dagsins UI using an anonymous closure component
36401
- m.mount(container, {
36402
- view: () => m(GataDagsins$1, { view, date: today, locale }),
36403
- });
36404
- return "success";
36405
- }
36474
+ // Skip initial login - authentication will happen lazily on first API call
36475
+ // Create the model, view and actions objects
36476
+ const settings = getSettings();
36477
+ const model = new Model(settings, state);
36478
+ const actions = new Actions(model);
36479
+ const view = new View(actions);
36480
+ const today = new Date().toISOString().split("T")[0];
36481
+ const locale = state.locale || "is_IS";
36482
+ // Mount the Gáta Dagsins UI using an anonymous closure component
36483
+ m.mount(container, {
36484
+ view: () => m(GataDagsins$1, { view, date: today, locale }),
36485
+ });
36406
36486
  }
36407
36487
  catch (e) {
36408
- console.error("Exception during login: ", e);
36488
+ console.error("Exception during initialization: ", e);
36489
+ return "error";
36409
36490
  }
36410
- m.mount(container, LoginError);
36411
- return "error";
36491
+ return "success";
36412
36492
  }
36413
36493
 
36414
- const mountForUser = async (state, tokenExpired) => {
36494
+ const mountForUser = async (state) => {
36415
36495
  // Return a DOM tree containing a mounted Gáta Dagsins UI
36416
36496
  // for the user specified in the state object
36417
36497
  const { userEmail } = state;
@@ -36439,18 +36519,12 @@ const mountForUser = async (state, tokenExpired) => {
36439
36519
  root.className = "gatadagsins-user";
36440
36520
  return root;
36441
36521
  }
36442
- else if (loginResult === "expired") {
36443
- // We need a new token from the Málstaður backend
36444
- root.className = "gatadagsins-expired";
36445
- tokenExpired && tokenExpired(); // This causes a reload of the component
36446
- return root;
36447
- }
36448
36522
  // console.error("Failed to mount Gáta Dagsins UI for user", userEmail);
36449
36523
  throw new Error("Failed to mount Gáta Dagsins UI");
36450
36524
  };
36451
36525
  const GataDagsinsImpl = ({ state, tokenExpired }) => {
36452
36526
  const ref = React.createRef();
36453
- const completeState = { ...DEFAULT_STATE, ...state };
36527
+ const completeState = makeGlobalState({ ...state, tokenExpired });
36454
36528
  const { userEmail } = completeState;
36455
36529
  useEffect(() => {
36456
36530
  var _a;
@@ -36468,7 +36542,7 @@ const GataDagsinsImpl = ({ state, tokenExpired }) => {
36468
36542
  return;
36469
36543
  }
36470
36544
  try {
36471
- mountForUser(completeState, tokenExpired).then((div) => {
36545
+ mountForUser(completeState).then((div) => {
36472
36546
  // Attach the div as a child of the container
36473
36547
  // instead of any previous children
36474
36548
  const container = ref.current;