@mideind/netskrafl-react 1.0.0-beta.9 → 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,53 +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 url = `${MOVES_SERVER_PREFIX}${options.url}`;
2223
- const headers = {
2224
- "Content-Type": "application/json; charset=UTF-8",
2225
- Authorization: `Bearer ${MOVES_ACCESS_KEY}`,
2226
- ...options === null || options === void 0 ? void 0 : options.headers,
2227
- };
2228
- return m.request({
2229
- withCredentials: false,
2230
- method: "POST",
2231
- ...options,
2232
- url,
2233
- headers,
2234
- });
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"
2235
2434
  };
2236
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
+
2237
2690
  /**
2238
2691
  * @license
2239
2692
  * Copyright 2017 Google LLC
@@ -26559,33 +27012,46 @@ let database;
26559
27012
  let analytics;
26560
27013
  function initFirebase(state) {
26561
27014
  try {
26562
- const projectId = state.projectId;
27015
+ const { projectId, firebaseAPIKey, databaseURL, firebaseSenderId, firebaseAppId, measurementId } = state;
26563
27016
  const firebaseOptions = {
26564
27017
  projectId,
26565
- apiKey: state.firebaseAPIKey,
27018
+ apiKey: firebaseAPIKey,
26566
27019
  authDomain: `${projectId}.firebaseapp.com`,
26567
- databaseURL: state.databaseURL,
27020
+ databaseURL,
26568
27021
  storageBucket: `${projectId}.firebasestorage.app`,
26569
- messagingSenderId: state.firebaseSenderId,
26570
- appId: state.firebaseAppId,
26571
- measurementId: state.measurementId,
27022
+ messagingSenderId: firebaseSenderId,
27023
+ appId: firebaseAppId,
27024
+ measurementId,
26572
27025
  };
26573
- app = initializeApp(firebaseOptions, "netskrafl");
27026
+ app = initializeApp(firebaseOptions, projectId);
26574
27027
  if (!app) {
26575
27028
  console.error("Failed to initialize Firebase");
27029
+ return false;
26576
27030
  }
26577
27031
  }
26578
27032
  catch (e) {
26579
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);
26580
27046
  }
27047
+ if (!auth)
27048
+ return false;
27049
+ return auth.currentUser !== null;
26581
27050
  }
26582
27051
  async function loginFirebase(state, firebaseToken, onLoginFunc) {
26583
- if (!app)
26584
- initFirebase(state);
26585
- if (!app)
27052
+ if (!app && !initFirebase(state))
26586
27053
  return;
26587
- const userId = state.userId;
26588
- const locale = state.locale;
27054
+ const { userId, locale, projectId, loginMethod } = state;
26589
27055
  auth = getAuth(app);
26590
27056
  if (!auth) {
26591
27057
  console.error("Failed to initialize Firebase Auth");
@@ -26598,16 +27064,16 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
26598
27064
  // For new users, log an additional signup event
26599
27065
  if (state.newUser) {
26600
27066
  logEvent("sign_up", {
26601
- locale: state.locale,
26602
- method: state.loginMethod,
26603
- userid: state.userId
27067
+ locale,
27068
+ method: loginMethod,
27069
+ userid: userId
26604
27070
  });
26605
27071
  }
26606
27072
  // And always log a login event
26607
27073
  logEvent("login", {
26608
- locale: state.locale,
26609
- method: state.loginMethod,
26610
- userid: state.userId
27074
+ locale,
27075
+ method: loginMethod,
27076
+ userid: userId
26611
27077
  });
26612
27078
  }
26613
27079
  });
@@ -26622,7 +27088,7 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
26622
27088
  if (!analytics) {
26623
27089
  console.error("Failed to initialize Firebase Analytics");
26624
27090
  }
26625
- initPresence(state.projectId, userId, locale);
27091
+ initPresence(projectId, userId, locale);
26626
27092
  }
26627
27093
  function initPresence(projectId, userId, locale) {
26628
27094
  // Ensure that this user connection is recorded in Firebase
@@ -26679,473 +27145,6 @@ function logEvent(ev, params) {
26679
27145
  logEvent$2(analytics, ev, params);
26680
27146
  }
26681
27147
 
26682
- /*
26683
-
26684
- i8n.ts
26685
-
26686
- Single page UI for Netskrafl/Explo using the Mithril library
26687
-
26688
- Copyright (C) 2025 Miðeind ehf.
26689
- Author: Vilhjálmur Þorsteinsson
26690
-
26691
- The Creative Commons Attribution-NonCommercial 4.0
26692
- International Public License (CC-BY-NC 4.0) applies to this software.
26693
- For further information, see https://github.com/mideind/Netskrafl
26694
-
26695
-
26696
- This module contains internationalization (i18n) utility functions,
26697
- allowing for translation of displayed text between languages.
26698
-
26699
- Text messages for individual locales are loaded from the
26700
- /static/assets/messages.json file, which is fetched from the server.
26701
-
26702
- */
26703
- // Current exact user locale and fallback locale ("en" for "en_US"/"en_GB"/...)
26704
- // This is overwritten in setLocale()
26705
- let currentLocale = "is_IS";
26706
- let currentFallback = "is";
26707
- // Regex that matches embedded interpolations such as "Welcome, {username}!"
26708
- // Interpolation identifiers should only contain ASCII characters, digits and '_'
26709
- const rex = /{\s*(\w+)\s*}/g;
26710
- let messages = {};
26711
- let messagesLoaded = false;
26712
- function hasAnyTranslation(msgs, locale) {
26713
- // Return true if any translation is available for the given locale
26714
- for (let key in msgs) {
26715
- if (msgs[key][locale] !== undefined)
26716
- return true;
26717
- }
26718
- return false;
26719
- }
26720
- function setLocale(locale, msgs) {
26721
- // Set the current i18n locale and fallback
26722
- currentLocale = locale;
26723
- currentFallback = locale.split("_")[0];
26724
- // For unsupported locales, i.e. locales that have no
26725
- // translations available for them, fall back to English (U.S.).
26726
- if (!hasAnyTranslation(msgs, currentLocale) && !hasAnyTranslation(msgs, currentFallback)) {
26727
- currentLocale = "en_US";
26728
- currentFallback = "en";
26729
- }
26730
- // Flatten the Messages structure, enabling long strings
26731
- // to be represented as string arrays in the messages.json file
26732
- messages = {};
26733
- for (let key in msgs) {
26734
- for (let lc in msgs[key]) {
26735
- let s = msgs[key][lc];
26736
- if (Array.isArray(s))
26737
- s = s.join("");
26738
- if (messages[key] === undefined)
26739
- messages[key] = {};
26740
- // If the string s contains HTML markup of the form <tag>...</tag>,
26741
- // convert it into a list of Mithril Vnode children corresponding to
26742
- // the text and the tags
26743
- if (s.match(/<[a-z]+>/)) {
26744
- // Looks like the string contains HTML markup
26745
- const vnodes = [];
26746
- let i = 0;
26747
- let tagMatch = null;
26748
- while (i < s.length && (tagMatch = s.slice(i).match(/<[a-z]+>/)) && tagMatch.index !== undefined) {
26749
- // Found what looks like an HTML tag
26750
- // Calculate the index of the enclosed text within s
26751
- const tag = tagMatch[0];
26752
- let j = i + tagMatch.index + tag.length;
26753
- // Find the end tag
26754
- let end = s.indexOf("</" + tag.slice(1), j);
26755
- if (end < 0) {
26756
- // No end tag - skip past this weirdness
26757
- i = j;
26758
- continue;
26759
- }
26760
- // Add the text preceding the tag
26761
- if (tagMatch.index > 0)
26762
- vnodes.push(s.slice(i, i + tagMatch.index));
26763
- // Create the Mithril node corresponding to the tag and the enclosed text
26764
- // and add it to the list
26765
- vnodes.push(m(tag.slice(1, -1), s.slice(j, end)));
26766
- // Advance the index past the end of the tag
26767
- i = end + tag.length + 1;
26768
- }
26769
- // Push the final text part, if any
26770
- if (i < s.length)
26771
- vnodes.push(s.slice(i));
26772
- // Reassign s to the list of vnodes
26773
- s = vnodes;
26774
- }
26775
- messages[key][lc] = s;
26776
- }
26777
- }
26778
- messagesLoaded = true;
26779
- }
26780
- async function loadMessages(locale) {
26781
- // Load the internationalization message JSON file from the server
26782
- // and set the user's locale
26783
- try {
26784
- const messages = await request({
26785
- method: "GET",
26786
- url: "/static/assets/messages.json",
26787
- withCredentials: false, // Cookies are not allowed for CORS request
26788
- });
26789
- setLocale(locale, messages);
26790
- }
26791
- catch (_a) {
26792
- setLocale(locale, {});
26793
- }
26794
- }
26795
- function t(key, ips = {}) {
26796
- // Main text translation function, supporting interpolation
26797
- // and HTML tag substitution
26798
- const msgDict = messages[key];
26799
- if (msgDict === undefined)
26800
- // No dictionary for this key - may actually be a missing entry
26801
- return messagesLoaded ? key : "";
26802
- // Lookup exact locale, then fallback, then resort to returning the key
26803
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
26804
- // If we have an interpolation object, do the interpolation first
26805
- return Object.keys(ips).length ? interpolate(message, ips) : message;
26806
- }
26807
- function ts(key, ips = {}) {
26808
- // String translation function, supporting interpolation
26809
- // but not HTML tag substitution
26810
- const msgDict = messages[key];
26811
- if (msgDict === undefined)
26812
- // No dictionary for this key - may actually be a missing entry
26813
- return messagesLoaded ? key : "";
26814
- // Lookup exact locale, then fallback, then resort to returning the key
26815
- const message = msgDict[currentLocale] || msgDict[currentFallback] || key;
26816
- if (typeof message != "string")
26817
- // This is actually an error - the client should be calling t() instead
26818
- return "";
26819
- // If we have an interpolation object, do the interpolation first
26820
- return Object.keys(ips).length ? interpolate_string(message, ips) : message;
26821
- }
26822
- function mt(cls, children) {
26823
- // Wrapper for the Mithril m() function that auto-translates
26824
- // string and array arguments
26825
- if (typeof children == "string") {
26826
- return m(cls, t(children));
26827
- }
26828
- if (Array.isArray(children)) {
26829
- return m(cls, children.map((item) => (typeof item == "string") ? t(item) : item));
26830
- }
26831
- return m(cls, children);
26832
- }
26833
- function interpolate(message, ips) {
26834
- // Replace interpolation placeholders with their corresponding values
26835
- if (typeof message == "string") {
26836
- return message.replace(rex, (match, key) => ips[key] || match);
26837
- }
26838
- if (Array.isArray(message)) {
26839
- return message.map((item) => interpolate(item, ips));
26840
- }
26841
- return message;
26842
- }
26843
- function interpolate_string(message, ips) {
26844
- // Replace interpolation placeholders with their corresponding values
26845
- return message.replace(rex, (match, key) => ips[key] || match);
26846
- }
26847
-
26848
- /*
26849
-
26850
- Types.ts
26851
-
26852
- Common type definitions for the Explo/Netskrafl user interface
26853
-
26854
- Copyright (C) 2025 Miðeind ehf.
26855
- Author: Vilhjalmur Thorsteinsson
26856
-
26857
- The Creative Commons Attribution-NonCommercial 4.0
26858
- International Public License (CC-BY-NC 4.0) applies to this software.
26859
- For further information, see https://github.com/mideind/Netskrafl
26860
-
26861
- */
26862
- // Global constants
26863
- const RACK_SIZE = 7;
26864
- const ROWIDS = "ABCDEFGHIJKLMNO";
26865
- const BOARD_SIZE = ROWIDS.length;
26866
- const EXTRA_WIDE_LETTERS = "q";
26867
- const WIDE_LETTERS = "zxmæ";
26868
- const ZOOM_FACTOR = 1.5;
26869
- const ERROR_MESSAGES = {
26870
- // Translations are found in /static/assets/messages.json
26871
- 1: "Enginn stafur lagður niður",
26872
- 2: "Fyrsta orð verður að liggja um byrjunarreitinn",
26873
- 3: "Orð verður að vera samfellt á borðinu",
26874
- 4: "Orð verður að tengjast orði sem fyrir er",
26875
- 5: "Reitur þegar upptekinn",
26876
- 6: "Ekki má vera eyða í orði",
26877
- 7: "word_not_found",
26878
- 8: "word_not_found",
26879
- 9: "Of margir stafir lagðir niður",
26880
- 10: "Stafur er ekki í rekkanum",
26881
- 11: "Of fáir stafir eftir, skipting ekki leyfð",
26882
- 12: "Of mörgum stöfum skipt",
26883
- 13: "Leik vantar á borðið - notið F5/Refresh",
26884
- 14: "Notandi ekki innskráður - notið F5/Refresh",
26885
- 15: "Rangur eða óþekktur notandi",
26886
- 16: "Viðureign finnst ekki",
26887
- 17: "Viðureign er ekki utan tímamarka",
26888
- 18: "Netþjónn gat ekki tekið við leiknum - reyndu aftur",
26889
- 19: "Véfenging er ekki möguleg í þessari viðureign",
26890
- 20: "Síðasti leikur er ekki véfengjanlegur",
26891
- 21: "Aðeins véfenging eða pass leyfileg",
26892
- "server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
26893
- };
26894
-
26895
- /*
26896
-
26897
- Util.ts
26898
-
26899
- Utility functions for the Explo/Netskrafl user interface
26900
-
26901
- Copyright (C) 2025 Miðeind ehf.
26902
- Author: Vilhjálmur Þorsteinsson
26903
-
26904
- The Creative Commons Attribution-NonCommercial 4.0
26905
- International Public License (CC-BY-NC 4.0) applies to this software.
26906
- For further information, see https://github.com/mideind/Netskrafl
26907
-
26908
- The following code is based on
26909
- https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events/Pinch_zoom_gestures
26910
-
26911
- */
26912
- // Global vars to cache event state
26913
- var evCache = [];
26914
- var origDistance = -1;
26915
- const PINCH_THRESHOLD = 10; // Minimum pinch movement
26916
- var hasZoomed = false;
26917
- // Old-style (non-single-page) game URL prefix
26918
- const BOARD_PREFIX = "/board?game=";
26919
- const BOARD_PREFIX_LEN = BOARD_PREFIX.length;
26920
- function addPinchZoom(attrs, funcZoomIn, funcZoomOut) {
26921
- // Install event handlers for the pointer target
26922
- attrs.onpointerdown = pointerdown_handler;
26923
- attrs.onpointermove = pointermove_handler.bind(null, funcZoomIn, funcZoomOut);
26924
- // Use same handler for pointer{up,cancel,out,leave} events since
26925
- // the semantics for these events - in this app - are the same.
26926
- attrs.onpointerup = pointerup_handler;
26927
- attrs.onpointercancel = pointerup_handler;
26928
- attrs.onpointerout = pointerup_handler;
26929
- attrs.onpointerleave = pointerup_handler;
26930
- }
26931
- function pointerdown_handler(ev) {
26932
- // The pointerdown event signals the start of a touch interaction.
26933
- // This event is cached to support 2-finger gestures
26934
- evCache.push(ev);
26935
- }
26936
- function pointermove_handler(funcZoomIn, funcZoomOut, ev) {
26937
- // This function implements a 2-pointer horizontal pinch/zoom gesture.
26938
- //
26939
- // If the distance between the two pointers has increased (zoom in),
26940
- // the target element's background is changed to "pink" and if the
26941
- // distance is decreasing (zoom out), the color is changed to "lightblue".
26942
- //
26943
- // Find this event in the cache and update its record with this event
26944
- for (let i = 0; i < evCache.length; i++) {
26945
- if (ev.pointerId === evCache[i].pointerId) {
26946
- evCache[i] = ev;
26947
- break;
26948
- }
26949
- }
26950
- // If two pointers are down, check for pinch gestures
26951
- if (evCache.length == 2) {
26952
- // Calculate the distance between the two pointers
26953
- const curDistance = Math.sqrt(Math.pow(evCache[0].clientX - evCache[1].clientX, 2) +
26954
- Math.pow(evCache[0].clientY - evCache[1].clientY, 2));
26955
- if (origDistance > 0) {
26956
- if (curDistance - origDistance >= PINCH_THRESHOLD) {
26957
- // The distance between the two pointers has increased
26958
- if (!hasZoomed)
26959
- funcZoomIn();
26960
- hasZoomed = true;
26961
- }
26962
- else if (origDistance - curDistance >= PINCH_THRESHOLD) {
26963
- // The distance between the two pointers has decreased
26964
- if (!hasZoomed)
26965
- funcZoomOut();
26966
- hasZoomed = true;
26967
- }
26968
- }
26969
- else if (origDistance < 0) {
26970
- // Note the original difference between two pointers
26971
- origDistance = curDistance;
26972
- hasZoomed = false;
26973
- }
26974
- }
26975
- }
26976
- function pointerup_handler(ev) {
26977
- // Remove this pointer from the cache and reset the target's
26978
- // background and border
26979
- remove_event(ev);
26980
- // If the number of pointers down is less than two then reset diff tracker
26981
- if (evCache.length < 2) {
26982
- origDistance = -1;
26983
- }
26984
- }
26985
- function remove_event(ev) {
26986
- // Remove this event from the target's cache
26987
- for (let i = 0; i < evCache.length; i++) {
26988
- if (evCache[i].pointerId === ev.pointerId) {
26989
- evCache.splice(i, 1);
26990
- break;
26991
- }
26992
- }
26993
- }
26994
- function buttonOver(ev) {
26995
- const clist = ev.currentTarget.classList;
26996
- if (clist !== undefined && !clist.contains("disabled"))
26997
- clist.add("over");
26998
- ev.redraw = false;
26999
- }
27000
- function buttonOut(ev) {
27001
- const clist = ev.currentTarget.classList;
27002
- if (clist !== undefined)
27003
- clist.remove("over");
27004
- ev.redraw = false;
27005
- }
27006
- // Glyphicon utility function: inserts a glyphicon span
27007
- function glyph(icon, attrs, grayed) {
27008
- return m("span.glyphicon.glyphicon-" + icon + (grayed ? ".grayed" : ""), attrs);
27009
- }
27010
- function glyphGrayed(icon, attrs) {
27011
- return m("span.glyphicon.glyphicon-" + icon + ".grayed", attrs);
27012
- }
27013
- // Utility function: inserts non-breaking space
27014
- function nbsp(n) {
27015
- return m.trust("&nbsp;");
27016
- }
27017
- // Utility functions
27018
- function escapeHtml(string) {
27019
- /* Utility function to properly encode a string into HTML */
27020
- const entityMap = {
27021
- "&": "&amp;",
27022
- "<": "&lt;",
27023
- ">": "&gt;",
27024
- '"': '&quot;',
27025
- "'": '&#39;',
27026
- "/": '&#x2F;'
27027
- };
27028
- return String(string).replace(/[&<>"'/]/g, (s) => { var _a; return (_a = entityMap[s]) !== null && _a !== void 0 ? _a : ""; });
27029
- }
27030
- function getUrlVars(url) {
27031
- // Get values from a URL query string
27032
- const hashes = url.split('&');
27033
- const vars = {};
27034
- for (let i = 0; i < hashes.length; i++) {
27035
- const hash = hashes[i].split('=');
27036
- if (hash.length == 2)
27037
- vars[hash[0]] = decodeURIComponent(hash[1]);
27038
- }
27039
- return vars;
27040
- }
27041
- function getInput(id) {
27042
- // Return the current value of a text input field
27043
- const elem = document.getElementById(id);
27044
- return elem.value;
27045
- }
27046
- function setInput(id, val) {
27047
- // Set the current value of a text input field
27048
- const elem = document.getElementById(id);
27049
- elem.value = val;
27050
- }
27051
- function playAudio(elemId) {
27052
- // Play an audio file
27053
- const sound = document.getElementById(elemId);
27054
- if (sound)
27055
- sound.play();
27056
- }
27057
- function arrayEqual(a, b) {
27058
- // Return true if arrays a and b are equal
27059
- if (a.length != b.length)
27060
- return false;
27061
- for (let i = 0; i < a.length; i++)
27062
- if (a[i] != b[i])
27063
- return false;
27064
- return true;
27065
- }
27066
- function gameUrl(url) {
27067
- // Convert old-style game URL to new-style single-page URL
27068
- // The URL format is "/board?game=ed27b9f0-d429-11eb-8bc7-d43d7ee303b2&zombie=1"
27069
- if (url.slice(0, BOARD_PREFIX_LEN) == BOARD_PREFIX)
27070
- // Cut off "/board?game="
27071
- url = url.slice(BOARD_PREFIX_LEN);
27072
- // Isolate the game UUID
27073
- const uuid = url.slice(0, 36);
27074
- // Isolate the other parameters, if any
27075
- let params = url.slice(36);
27076
- // Start parameter section of URL with a ? sign
27077
- if (params.length > 0 && params.charAt(0) == "&")
27078
- params = "?" + params.slice(1);
27079
- // Return the single-page URL, to be consumed by m.route.Link()
27080
- return "/game/" + uuid + params;
27081
- }
27082
- function scrollMovelistToBottom() {
27083
- // If the length of the move list has changed,
27084
- // scroll the last move into view
27085
- let movelist = document.querySelectorAll("div.movelist .move");
27086
- if (!movelist || !movelist.length)
27087
- return;
27088
- let target = movelist[movelist.length - 1];
27089
- let parent = target.parentNode;
27090
- let len = parent.getAttribute("data-len");
27091
- let intLen = (!len) ? 0 : parseInt(len);
27092
- if (movelist.length > intLen) {
27093
- // The list has grown since we last updated it:
27094
- // scroll to the bottom and mark its length
27095
- parent.scrollTop = target.offsetTop;
27096
- }
27097
- parent.setAttribute("data-len", movelist.length.toString());
27098
- }
27099
- function coord(row, col, vertical = false) {
27100
- // Return the co-ordinate string for the given 0-based row and col
27101
- if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE)
27102
- return null;
27103
- // Horizontal moves have the row letter first
27104
- // Vertical moves have the column number first
27105
- return vertical ? `${col + 1}${ROWIDS[row]}` : `${ROWIDS[row]}${col + 1}`;
27106
- }
27107
- function toVector(co) {
27108
- // Convert a co-ordinate string to a 0-based row, col and direction vector
27109
- var dx = 0, dy = 0;
27110
- var col = 0;
27111
- var row = ROWIDS.indexOf(co[0]);
27112
- if (row >= 0) {
27113
- /* Horizontal move */
27114
- col = parseInt(co.slice(1)) - 1;
27115
- dx = 1;
27116
- }
27117
- else {
27118
- /* Vertical move */
27119
- row = ROWIDS.indexOf(co.slice(-1));
27120
- col = parseInt(co) - 1;
27121
- dy = 1;
27122
- }
27123
- return { col: col, row: row, dx: dx, dy: dy };
27124
- }
27125
- function valueOrK(value, breakpoint = 10000) {
27126
- // Return a numeric value as a string, but in kilos (thousands)
27127
- // if it exceeds a breakpoint, in that case suffixed by "K"
27128
- const sign = value < 0 ? "-" : "";
27129
- value = Math.abs(value);
27130
- if (value < breakpoint)
27131
- return `${sign}${value}`;
27132
- value = Math.round(value / 1000);
27133
- return `${sign}${value}K`;
27134
- }
27135
- // SalesCloud stuff
27136
- function doRegisterSalesCloud(i, s, o, g, r, a, m) {
27137
- i.SalesCloudObject = r;
27138
- i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments); };
27139
- i[r].l = 1 * new Date();
27140
- a = s.createElement(o);
27141
- m = s.getElementsByTagName(o)[0];
27142
- a.src = g;
27143
- m.parentNode.insertBefore(a, m);
27144
- }
27145
- function registerSalesCloud() {
27146
- doRegisterSalesCloud(window, document, 'script', 'https://cdn.salescloud.is/js/salescloud.min.js', 'salescloud');
27147
- }
27148
-
27149
27148
  /*
27150
27149
 
27151
27150
  Logo.ts
@@ -27255,7 +27254,7 @@ const NetskraflLegend = (initialVnode) => {
27255
27254
  m.redraw();
27256
27255
  }
27257
27256
  return {
27258
- oncreate: () => {
27257
+ oninit: () => {
27259
27258
  if (msStepTime && ival === 0) {
27260
27259
  ival = setInterval(doStep, msStepTime);
27261
27260
  }
@@ -27310,10 +27309,197 @@ const AnimatedNetskraflLogo = (initialVnode) => {
27310
27309
  };
27311
27310
  };
27312
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
+
27313
27499
  const SPINNER_INITIAL_DELAY = 800; // milliseconds
27314
27500
  const Spinner = {
27315
27501
  // Show a spinner wait box, after an initial delay
27316
- oncreate: (vnode) => {
27502
+ oninit: (vnode) => {
27317
27503
  vnode.state.show = false;
27318
27504
  vnode.state.ival = setTimeout(() => {
27319
27505
  vnode.state.show = true;
@@ -27426,11 +27612,12 @@ const OnlinePresence = (initialVnode) => {
27426
27612
  const askServer = attrs.online === undefined;
27427
27613
  const id = attrs.id;
27428
27614
  const userId = attrs.userId;
27615
+ const state = attrs.state;
27429
27616
  let loading = false;
27430
27617
  async function _update() {
27431
27618
  if (askServer && !loading) {
27432
27619
  loading = true;
27433
- const json = await request({
27620
+ const json = await request(state, {
27434
27621
  method: "POST",
27435
27622
  url: "/onlinecheck",
27436
27623
  body: { user: userId }
@@ -27440,7 +27627,7 @@ const OnlinePresence = (initialVnode) => {
27440
27627
  }
27441
27628
  }
27442
27629
  return {
27443
- oncreate: _update,
27630
+ oninit: _update,
27444
27631
  view: (vnode) => {
27445
27632
  var _a, _b;
27446
27633
  if (!askServer)
@@ -27706,6 +27893,7 @@ const WaitDialog = (initialVnode) => {
27706
27893
  const attrs = initialVnode.attrs;
27707
27894
  const view = attrs.view;
27708
27895
  const model = view.model;
27896
+ const state = model.state;
27709
27897
  const duration = attrs.duration;
27710
27898
  const oppId = attrs.oppId;
27711
27899
  const key = attrs.challengeKey;
@@ -27721,9 +27909,9 @@ const WaitDialog = (initialVnode) => {
27721
27909
  async function updateOnline() {
27722
27910
  // Initiate an online check on the opponent
27723
27911
  try {
27724
- if (!oppId || !key)
27912
+ if (!oppId || !key || !state)
27725
27913
  return;
27726
- const json = await request({
27914
+ const json = await request(state, {
27727
27915
  method: "POST",
27728
27916
  url: "/initwait",
27729
27917
  body: { opp: oppId, key }
@@ -27739,8 +27927,10 @@ const WaitDialog = (initialVnode) => {
27739
27927
  }
27740
27928
  async function cancelWait() {
27741
27929
  // Cancel a pending wait for a timed game
27930
+ if (!state)
27931
+ return;
27742
27932
  try {
27743
- await request({
27933
+ await request(state, {
27744
27934
  method: "POST",
27745
27935
  url: "/cancelwait",
27746
27936
  body: {
@@ -27774,11 +27964,13 @@ const WaitDialog = (initialVnode) => {
27774
27964
  return {
27775
27965
  oncreate,
27776
27966
  view: () => {
27967
+ if (!state)
27968
+ return null;
27777
27969
  return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
27778
27970
  m(".chall-hdr", m("table", m("tbody", m("tr", [
27779
27971
  m("td", m("h1.chall-icon", glyph("time"))),
27780
27972
  m("td.l-border", [
27781
- m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline }),
27973
+ m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
27782
27974
  m("h1", oppNick),
27783
27975
  m("h2", oppName)
27784
27976
  ])
@@ -27821,6 +28013,7 @@ const AcceptDialog = (initialVnode) => {
27821
28013
  // is linked up with her opponent and a new game is started
27822
28014
  const attrs = initialVnode.attrs;
27823
28015
  const view = attrs.view;
28016
+ const state = view.model.state;
27824
28017
  const oppId = attrs.oppId;
27825
28018
  const key = attrs.challengeKey;
27826
28019
  let oppNick = attrs.oppNick;
@@ -27828,11 +28021,11 @@ const AcceptDialog = (initialVnode) => {
27828
28021
  let loading = false;
27829
28022
  async function waitCheck() {
27830
28023
  // Initiate a wait status check on the opponent
27831
- if (loading)
28024
+ if (loading || !state)
27832
28025
  return; // Already checking
27833
28026
  loading = true;
27834
28027
  try {
27835
- const json = await request({
28028
+ const json = await request(state, {
27836
28029
  method: "POST",
27837
28030
  url: "/waitcheck",
27838
28031
  body: { user: oppId, key }
@@ -27855,7 +28048,7 @@ const AcceptDialog = (initialVnode) => {
27855
28048
  }
27856
28049
  }
27857
28050
  return {
27858
- oncreate: waitCheck,
28051
+ oninit: waitCheck,
27859
28052
  view: () => {
27860
28053
  return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
27861
28054
  m(".chall-hdr", m("table", m("tbody", m("tr", [
@@ -28040,87 +28233,6 @@ const FriendCancelConfirmDialog = (initialVnode) => {
28040
28233
  };
28041
28234
  };
28042
28235
 
28043
- /*
28044
-
28045
- Login.ts
28046
-
28047
- Login UI for Metskrafl using the Mithril library
28048
-
28049
- Copyright (C) 2025 Miðeind ehf.
28050
- Author: Vilhjálmur Þorsteinsson
28051
-
28052
- The Creative Commons Attribution-NonCommercial 4.0
28053
- International Public License (CC-BY-NC 4.0) applies to this software.
28054
- For further information, see https://github.com/mideind/Netskrafl
28055
-
28056
- This UI is built on top of Mithril (https://mithril.js.org), a lightweight,
28057
- straightforward JavaScript single-page reactive UI library.
28058
-
28059
- */
28060
- const loginUserByEmail = async (email, nickname, fullname, token) => {
28061
- // Call the /login_malstadur endpoint on the server
28062
- // to log in the user with the given email and token.
28063
- // The token is a standard HS256-encoded JWT with aud "netskrafl"
28064
- // and iss typically "malstadur".
28065
- return request({
28066
- method: "POST",
28067
- url: "/login_malstadur",
28068
- body: { email, nickname, fullname, token }
28069
- });
28070
- };
28071
- const LoginError = {
28072
- view: (vnode) => {
28073
- var _a;
28074
- return m("div.error", { style: { visibility: "visible" } }, ((_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.message) || "Error logging in");
28075
- }
28076
- };
28077
- const LoginForm = (initialVnode) => {
28078
- const loginUrl = initialVnode.attrs.loginUrl;
28079
- let loginInProgress = false;
28080
- function doLogin(ev) {
28081
- loginInProgress = true;
28082
- ev.preventDefault();
28083
- window.location.href = loginUrl;
28084
- }
28085
- return {
28086
- view: () => {
28087
- return m.fragment({}, [
28088
- // This is visible on large screens
28089
- m("div.loginform-large", [
28090
- m(NetskraflLogoOnly, {
28091
- className: "login-logo",
28092
- width: 200,
28093
- }),
28094
- m(NetskraflLegend, {
28095
- className: "login-legend",
28096
- width: 600,
28097
- msStepTime: 0
28098
- }),
28099
- mt("div.welcome", "welcome_0"),
28100
- mt("div.welcome", "welcome_1"),
28101
- mt("div.welcome", "welcome_2"),
28102
- m("div.login-btn-large", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : [
28103
- t("Innskrá") + " ", m("span.glyphicon.glyphicon-play")
28104
- ])
28105
- ]),
28106
- // This is visible on small screens
28107
- m("div.loginform-small", [
28108
- m(NetskraflLogoOnly, {
28109
- className: "login-logo",
28110
- width: 160,
28111
- }),
28112
- m(NetskraflLegend, {
28113
- className: "login-legend",
28114
- width: 650,
28115
- msStepTime: 0
28116
- }),
28117
- m("div.login-btn-small", { onclick: doLogin }, loginInProgress ? t("Skrái þig inn...") : t("Innskrá"))
28118
- ])
28119
- ]);
28120
- }
28121
- };
28122
- };
28123
-
28124
28236
  /*
28125
28237
 
28126
28238
  ChallengeDialog.ts
@@ -28151,7 +28263,11 @@ const ChallengeDialog = () => {
28151
28263
  m(".chall-hdr", m("table", m("tbody", m("tr", [
28152
28264
  m("td", m("h1.chall-icon", glyph("hand-right"))),
28153
28265
  m("td.l-border", [
28154
- m(OnlinePresence, { id: "chall-online", userId: item.userid }),
28266
+ m(OnlinePresence, {
28267
+ id: "chall-online",
28268
+ userId: item.userid,
28269
+ state,
28270
+ }),
28155
28271
  m("h1", item.nick),
28156
28272
  m("h2", item.fullname)
28157
28273
  ])
@@ -29467,7 +29583,7 @@ const PromoDialog = (initialVnode) => {
29467
29583
  initFunc();
29468
29584
  }
29469
29585
  return {
29470
- oncreate: _fetchContent,
29586
+ oninit: _fetchContent,
29471
29587
  view: (vnode) => {
29472
29588
  let initFunc = vnode.attrs.initFunc;
29473
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", {
@@ -29539,7 +29655,7 @@ const UserInfoDialog = (initialVnode) => {
29539
29655
  }
29540
29656
  }
29541
29657
  return {
29542
- oncreate: (vnode) => {
29658
+ oninit: (vnode) => {
29543
29659
  _updateRecentList(vnode);
29544
29660
  _updateStats(vnode);
29545
29661
  },
@@ -30529,7 +30645,9 @@ const Board = (initialVnode) => {
30529
30645
  const scale = view.boardScale || 1.0;
30530
30646
  let attrs = {};
30531
30647
  // Add handlers for pinch zoom functionality
30532
- 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());
30533
30651
  if (scale !== 1.0)
30534
30652
  attrs.style = `transform: scale(${scale})`;
30535
30653
  return m(".board", { id: "board-parent" }, m("table.board", attrs, m("tbody", allrows())));
@@ -30707,7 +30825,7 @@ const Chat = (initialVnode) => {
30707
30825
  const { view } = initialVnode.attrs;
30708
30826
  const model = view.model;
30709
30827
  const game = model.game;
30710
- model.state;
30828
+ const state = model.state;
30711
30829
  function decodeTimestamp(ts) {
30712
30830
  // Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
30713
30831
  return {
@@ -30764,7 +30882,7 @@ const Chat = (initialVnode) => {
30764
30882
  for (const emoticon of EMOTICONS)
30765
30883
  if (str.indexOf(emoticon.icon) >= 0) {
30766
30884
  // The string contains the emoticon: prepare to replace all occurrences
30767
- const imgUrl = serverUrl(emoticon.image);
30885
+ const imgUrl = serverUrl(state, emoticon.image);
30768
30886
  const img = `<img src='${imgUrl}' height='32' width='32'>`;
30769
30887
  // Re the following trick, see https://stackoverflow.com/questions/1144783/
30770
30888
  // replacing-all-occurrences-of-a-string-in-javascript
@@ -31915,13 +32033,13 @@ class View {
31915
32033
  views.push(this.vwLogin());
31916
32034
  break;
31917
32035
  case "loginerror":
31918
- views.push(m(LoginError));
32036
+ views.push(m(LoginError, { key: "login-error" }, t("Villa við innskráningu")));
31919
32037
  break;
31920
32038
  case "main":
31921
- views.push(m(Main, { view: this }));
32039
+ views.push(m(Main, { key: "main", view: this }));
31922
32040
  break;
31923
32041
  case "game":
31924
- views.push(m(GameView, { view: this }));
32042
+ views.push(m(GameView, { key: "game", view: this }));
31925
32043
  break;
31926
32044
  case "review":
31927
32045
  const n = vwReview(this);
@@ -31929,7 +32047,7 @@ class View {
31929
32047
  break;
31930
32048
  case "thanks":
31931
32049
  // Display a thank-you dialog on top of the normal main screen
31932
- views.push(m(Main, { view: this }));
32050
+ views.push(m(Main, { key: "main", view: this }));
31933
32051
  // Be careful to add the Thanks dialog only once to the stack
31934
32052
  if (!this.dialogStack.length)
31935
32053
  this.showThanks();
@@ -31940,7 +32058,7 @@ class View {
31940
32058
  views.push(this.vwHelp(parseInt(m.route.param("tab") || ""), parseInt(m.route.param("faq") || "")));
31941
32059
  break;
31942
32060
  default:
31943
- return [m("div", t("Þessi vefslóð er ekki rétt"))];
32061
+ return [m("div", { key: "error" }, t("Þessi vefslóð er ekki rétt"))];
31944
32062
  }
31945
32063
  // Push any open dialogs
31946
32064
  for (const dialog of this.dialogStack) {
@@ -31955,7 +32073,7 @@ class View {
31955
32073
  }
31956
32074
  // Overlay a spinner, if active
31957
32075
  if (model.spinners)
31958
- views.push(m(Spinner));
32076
+ views.push(m(Spinner, { key: "spinner" }));
31959
32077
  return views;
31960
32078
  }
31961
32079
  // Dialog support
@@ -32036,7 +32154,7 @@ class View {
32036
32154
  zoomOut() {
32037
32155
  if (this.boardScale !== 1.0) {
32038
32156
  this.boardScale = 1.0;
32039
- setTimeout(this.resetScale);
32157
+ setTimeout(() => this.resetScale());
32040
32158
  }
32041
32159
  }
32042
32160
  resetScale() {
@@ -32083,7 +32201,7 @@ class View {
32083
32201
  // No game or we're in full screen mode: always 100% scale
32084
32202
  // Also, as soon as a move is being processed by the server, we zoom out
32085
32203
  this.boardScale = 1.0; // Needs to be done before setTimeout() call
32086
- setTimeout(this.resetScale);
32204
+ setTimeout(() => this.resetScale());
32087
32205
  return;
32088
32206
  }
32089
32207
  const tp = game.tilesPlaced();
@@ -32177,7 +32295,7 @@ class View {
32177
32295
  }
32178
32296
  }
32179
32297
  // Output literal HTML obtained from rawhelp.html on the server
32180
- return m.fragment({}, [
32298
+ return m.fragment({ key: "help" }, [
32181
32299
  m(LeftLogo),
32182
32300
  m(UserId, { view: this }),
32183
32301
  this.vwTabsFromHtml(model.helpHTML || "", "tabs", tabNumber, wireQuestions),
@@ -32223,6 +32341,7 @@ class View {
32223
32341
  vnode.dom.querySelector("#nickname").focus();
32224
32342
  }
32225
32343
  return m(".modal-dialog", {
32344
+ key: "userprefs",
32226
32345
  id: "user-dialog",
32227
32346
  oncreate: initFocus
32228
32347
  // onupdate: initFocus
@@ -32304,11 +32423,12 @@ class View {
32304
32423
  model.loadUser(true); // Activate spinner while loading
32305
32424
  if (!model.user)
32306
32425
  // Nothing to edit (the spinner should be showing in this case)
32307
- return m.fragment({}, []);
32426
+ return m.fragment({ key: "userprefs-empty" }, []);
32308
32427
  return this.vwUserPrefsDialog();
32309
32428
  }
32310
32429
  vwUserInfo(args) {
32311
32430
  return m(UserInfoDialog, {
32431
+ key: "userinfodialog-" + args.userid,
32312
32432
  view: this,
32313
32433
  userid: args.userid,
32314
32434
  nick: args.nick,
@@ -32317,6 +32437,7 @@ class View {
32317
32437
  }
32318
32438
  vwPromo(args) {
32319
32439
  return m(PromoDialog, {
32440
+ key: "promo-" + args.kind,
32320
32441
  view: this,
32321
32442
  kind: args.kind,
32322
32443
  initFunc: args.initFunc
@@ -32324,6 +32445,7 @@ class View {
32324
32445
  }
32325
32446
  vwWait(args) {
32326
32447
  return m(WaitDialog, {
32448
+ key: "wait-" + args.challengeKey,
32327
32449
  view: this,
32328
32450
  oppId: args.oppId,
32329
32451
  oppNick: args.oppNick,
@@ -32334,6 +32456,7 @@ class View {
32334
32456
  }
32335
32457
  vwAccept(args) {
32336
32458
  return m(AcceptDialog, {
32459
+ key: "accept-" + args.challengeKey,
32337
32460
  view: this,
32338
32461
  oppId: args.oppId,
32339
32462
  oppNick: args.oppNick,
@@ -32344,7 +32467,7 @@ class View {
32344
32467
  var _a;
32345
32468
  const model = this.model;
32346
32469
  const loginUrl = ((_a = model.state) === null || _a === void 0 ? void 0 : _a.loginUrl) || "";
32347
- return m(LoginForm, { loginUrl });
32470
+ return m(LoginForm, { key: "login", loginUrl });
32348
32471
  }
32349
32472
  vwDialogs() {
32350
32473
  // Show prompt dialogs below game board, if any
@@ -32423,12 +32546,12 @@ class View {
32423
32546
  View.dialogViews = {
32424
32547
  userprefs: (view) => view.vwUserPrefs(),
32425
32548
  userinfo: (view, args) => view.vwUserInfo(args),
32426
- challenge: (view, args) => m(ChallengeDialog, { view, item: args }),
32549
+ challenge: (view, args) => m(ChallengeDialog, { key: "challenge-" + args.item.challenge_key, view, item: args }),
32427
32550
  promo: (view, args) => view.vwPromo(args),
32428
- friend: (view) => m(FriendPromoteDialog, { view }),
32429
- thanks: (view) => m(FriendThanksDialog, { view }),
32430
- cancel: (view) => m(FriendCancelDialog, { view }),
32431
- 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 }),
32432
32555
  wait: (view, args) => view.vwWait(args),
32433
32556
  accept: (view, args) => view.vwAccept(args)
32434
32557
  };
@@ -32466,7 +32589,7 @@ class WordChecker {
32466
32589
  }
32467
32590
  }
32468
32591
  }
32469
- async checkWords(locale, words) {
32592
+ async checkWords(state, locale, words) {
32470
32593
  // Return true if all words are valid in the given locale,
32471
32594
  // or false otherwise. Lookups are cached for efficiency.
32472
32595
  let cache = this.wordCheckCache[locale];
@@ -32494,7 +32617,7 @@ class WordChecker {
32494
32617
  }
32495
32618
  // We need a server roundtrip
32496
32619
  try {
32497
- const response = await requestMoves({
32620
+ const response = await requestMoves(state, {
32498
32621
  url: "/wordcheck",
32499
32622
  body: {
32500
32623
  locale,
@@ -32724,7 +32847,7 @@ const BOARD = {
32724
32847
  }
32725
32848
  };
32726
32849
  class BaseGame {
32727
- constructor(uuid, board_type = "standard") {
32850
+ constructor(uuid, state, board_type = "standard") {
32728
32851
  // Basic game properties that don't change while the game is underway
32729
32852
  this.locale = "is_IS";
32730
32853
  this.alphabet = "";
@@ -32758,6 +32881,7 @@ class BaseGame {
32758
32881
  this.board_type = board_type;
32759
32882
  this.startSquare = START_SQUARE[board_type];
32760
32883
  this.startCoord = START_COORD[board_type];
32884
+ this.state = state;
32761
32885
  }
32762
32886
  // Default init method that can be overridden
32763
32887
  init() {
@@ -32998,7 +33122,7 @@ class BaseGame {
32998
33122
  if (!this.manual) {
32999
33123
  // This is not a manual-wordcheck game:
33000
33124
  // Check the word that has been laid down
33001
- const found = await wordChecker.checkWords(this.locale, scoreResult.words);
33125
+ const found = await wordChecker.checkWords(this.state, this.locale, scoreResult.words);
33002
33126
  this.wordGood = found;
33003
33127
  this.wordBad = !found;
33004
33128
  }
@@ -33261,10 +33385,10 @@ const MAX_OVERTIME = 10 * 60.0;
33261
33385
  const DEBUG_OVERTIME = 1 * 60.0;
33262
33386
  const GAME_OVER = 99; // Error code corresponding to the Error class in skraflmechanics.py
33263
33387
  class Game extends BaseGame {
33264
- constructor(uuid, srvGame, moveListener, maxOvertime) {
33388
+ constructor(uuid, srvGame, moveListener, state, maxOvertime) {
33265
33389
  var _a;
33266
33390
  // Call parent constructor
33267
- super(uuid); // Default board_type: "standard"
33391
+ super(uuid, state); // Default board_type: "standard"
33268
33392
  // A class that represents a Netskrafl game instance on the client
33269
33393
  // Netskrafl-specific properties
33270
33394
  this.userid = ["", ""];
@@ -33521,7 +33645,7 @@ class Game extends BaseGame {
33521
33645
  try {
33522
33646
  if (!this.uuid)
33523
33647
  return;
33524
- const result = await request({
33648
+ const result = await request(this.state, {
33525
33649
  method: "POST",
33526
33650
  url: "/gamestate",
33527
33651
  body: { game: this.uuid } // !!! FIXME: Add delete_zombie parameter
@@ -33724,7 +33848,7 @@ class Game extends BaseGame {
33724
33848
  this.chatLoading = true;
33725
33849
  this.messages = [];
33726
33850
  try {
33727
- const result = await request({
33851
+ const result = await request(this.state, {
33728
33852
  method: "POST",
33729
33853
  url: "/chatload",
33730
33854
  body: { channel: "game:" + this.uuid }
@@ -33750,7 +33874,7 @@ class Game extends BaseGame {
33750
33874
  // Load statistics about a game
33751
33875
  this.stats = undefined; // Error/in-progress status
33752
33876
  try {
33753
- const json = await request({
33877
+ const json = await request(this.state, {
33754
33878
  method: "POST",
33755
33879
  url: "/gamestats",
33756
33880
  body: { game: this.uuid }
@@ -33770,7 +33894,7 @@ class Game extends BaseGame {
33770
33894
  async sendMessage(msg) {
33771
33895
  // Send a chat message
33772
33896
  try {
33773
- await request({
33897
+ await request(this.state, {
33774
33898
  method: "POST",
33775
33899
  url: "/chatmsg",
33776
33900
  body: { channel: "game:" + this.uuid, msg: msg }
@@ -33972,10 +34096,10 @@ class Game extends BaseGame {
33972
34096
  // Send a move to the server
33973
34097
  this.moveInProgress = true;
33974
34098
  try {
33975
- const result = await request({
34099
+ const result = await request(this.state, {
33976
34100
  method: "POST",
33977
34101
  url: "/submitmove",
33978
- body: { moves: moves, mcount: this.moves.length, uuid: this.uuid }
34102
+ body: { moves: moves, mcount: this.moves.length, uuid: this.uuid },
33979
34103
  });
33980
34104
  // The update() function also handles error results
33981
34105
  this.update(result);
@@ -34001,7 +34125,7 @@ class Game extends BaseGame {
34001
34125
  // Force resignation by a tardy opponent
34002
34126
  this.moveInProgress = true;
34003
34127
  try {
34004
- const result = await request({
34128
+ const result = await request(this.state, {
34005
34129
  method: "POST",
34006
34130
  url: "/forceresign",
34007
34131
  body: { mcount: this.moves.length, game: this.uuid }
@@ -34125,7 +34249,10 @@ const HOT_WARM_BOUNDARY_RATIO = 0.6;
34125
34249
  const WARM_COLD_BOUNDARY_RATIO = 0.3;
34126
34250
  class Riddle extends BaseGame {
34127
34251
  constructor(uuid, date, model) {
34128
- super(uuid);
34252
+ if (!model.state) {
34253
+ throw new Error("No global state in Riddle constructor");
34254
+ }
34255
+ super(uuid, model.state);
34129
34256
  // Scoring properties, static
34130
34257
  this.bestPossibleScore = 0;
34131
34258
  this.warmBoundary = 0;
@@ -34166,9 +34293,12 @@ class Riddle extends BaseGame {
34166
34293
  async load(date, locale) {
34167
34294
  this.date = date;
34168
34295
  this.locale = locale;
34296
+ const { state } = this;
34169
34297
  try {
34298
+ if (!state)
34299
+ throw new Error("No global state in Riddle.load");
34170
34300
  // Request riddle data from server (HTTP API call)
34171
- const response = await request({
34301
+ const response = await request(state, {
34172
34302
  method: "POST",
34173
34303
  url: "/gatadagsins/riddle",
34174
34304
  body: { date, locale }
@@ -34196,11 +34326,11 @@ class Riddle extends BaseGame {
34196
34326
  }
34197
34327
  }
34198
34328
  async submitRiddleWord(move) {
34199
- const { state } = this.model;
34329
+ const { state } = this;
34200
34330
  if (!state || !state.userId)
34201
34331
  return;
34202
34332
  try {
34203
- await request({
34333
+ await request(state, {
34204
34334
  method: "POST",
34205
34335
  url: "/gatadagsins/submit",
34206
34336
  body: {
@@ -34480,8 +34610,6 @@ function getSettings() {
34480
34610
  }
34481
34611
  class Model {
34482
34612
  constructor(settings, state) {
34483
- // A class for the underlying data model, displayed by the current view
34484
- this.state = null;
34485
34613
  this.paths = [];
34486
34614
  // The routeName will be "login", "main", "game"...
34487
34615
  this.routeName = undefined;
@@ -34540,7 +34668,54 @@ class Model {
34540
34668
  this.isExplo = state.isExplo;
34541
34669
  this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
34542
34670
  // Load localized text messages from the messages.json file
34543
- 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
+ });
34544
34719
  }
34545
34720
  async loadGame(uuid, funcComplete, deleteZombie = false) {
34546
34721
  var _a;
@@ -34559,20 +34734,16 @@ class Model {
34559
34734
  this.highlightedMove = null;
34560
34735
  if (!uuid)
34561
34736
  return; // Should not happen
34562
- const result = await request({
34563
- method: "POST",
34564
- url: "/gamestate",
34565
- body: {
34566
- game: uuid,
34567
- delete_zombie: deleteZombie
34568
- }
34737
+ const result = await this.post("/gamestate", {
34738
+ game: uuid,
34739
+ delete_zombie: deleteZombie
34569
34740
  });
34570
34741
  if (!(result === null || result === void 0 ? void 0 : result.ok)) {
34571
34742
  // console.log("Game " + uuid + " could not be loaded");
34572
34743
  }
34573
34744
  else {
34574
34745
  // Create a new game instance and load the state into it
34575
- 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);
34576
34747
  // Successfully loaded: call the completion function, if given
34577
34748
  // (this usually attaches the Firebase event listener)
34578
34749
  funcComplete && funcComplete();
@@ -34595,11 +34766,7 @@ class Model {
34595
34766
  this.numChallenges = 0;
34596
34767
  this.oppReady = 0;
34597
34768
  try {
34598
- const json = await request({
34599
- method: "POST",
34600
- url: "/allgamelists",
34601
- body: { zombie: includeZombies, count: 40 }
34602
- });
34769
+ const json = await this.post("/allgamelists", { zombie: includeZombies, count: 40 });
34603
34770
  if (!json || json.result !== 0) {
34604
34771
  // An error occurred
34605
34772
  this.gameList = [];
@@ -34641,11 +34808,7 @@ class Model {
34641
34808
  this.numGames = 0;
34642
34809
  this.spinners++;
34643
34810
  try {
34644
- const json = await request({
34645
- method: "POST",
34646
- url: "/gamelist",
34647
- body: { zombie: includeZombies }
34648
- });
34811
+ const json = await this.post("/gamelist", { zombie: includeZombies });
34649
34812
  if (!json || json.result !== 0) {
34650
34813
  // An error occurred
34651
34814
  this.gameList = [];
@@ -34675,10 +34838,7 @@ class Model {
34675
34838
  this.oppReady = 0;
34676
34839
  this.spinners++; // Show spinner while loading
34677
34840
  try {
34678
- const json = await request({
34679
- method: "POST",
34680
- url: "/challengelist"
34681
- });
34841
+ const json = await this.post("/challengelist");
34682
34842
  if (!json || json.result !== 0) {
34683
34843
  // An error occurred
34684
34844
  this.challengeList = [];
@@ -34712,11 +34872,7 @@ class Model {
34712
34872
  this.recentList = [];
34713
34873
  this.spinners++; // Show spinner while loading
34714
34874
  try {
34715
- const json = await request({
34716
- method: "POST",
34717
- url: "/recentlist",
34718
- body: { versus: null, count: 40 }
34719
- });
34875
+ const json = await this.post("/recentlist", { versus: null, count: 40 });
34720
34876
  if (!json || json.result !== 0) {
34721
34877
  // An error occurred
34722
34878
  this.recentList = [];
@@ -34735,11 +34891,7 @@ class Model {
34735
34891
  }
34736
34892
  async loadUserRecentList(userid, versus, readyFunc) {
34737
34893
  // Load the list of recent games for the given user
34738
- const json = await request({
34739
- method: "POST",
34740
- url: "/recentlist",
34741
- body: { user: userid, versus: versus, count: 40 }
34742
- });
34894
+ const json = await this.post("/recentlist", { user: userid, versus: versus, count: 40 });
34743
34895
  readyFunc(json);
34744
34896
  }
34745
34897
  async loadUserList(criteria) {
@@ -34757,11 +34909,7 @@ class Model {
34757
34909
  const url = "/userlist";
34758
34910
  const body = criteria;
34759
34911
  try {
34760
- const json = await request({
34761
- method: "POST",
34762
- url,
34763
- body,
34764
- });
34912
+ const json = await this.post(url, body);
34765
34913
  if (!json || json.result !== 0) {
34766
34914
  // An error occurred
34767
34915
  this.userList = [];
@@ -34783,11 +34931,7 @@ class Model {
34783
34931
  const url = "/rating";
34784
34932
  const body = { kind: spec };
34785
34933
  try {
34786
- const json = await request({
34787
- method: "POST",
34788
- url,
34789
- body,
34790
- });
34934
+ const json = await this.post(url, body);
34791
34935
  if (!json || json.result !== 0) {
34792
34936
  // An error occurred
34793
34937
  this.eloRatingList = [];
@@ -34806,11 +34950,7 @@ class Model {
34806
34950
  // Load statistics for the current user
34807
34951
  this.ownStats = {};
34808
34952
  try {
34809
- const json = await request({
34810
- method: "POST",
34811
- url: "/userstats",
34812
- body: {} // Current user is implicit
34813
- });
34953
+ const json = await this.post("/userstats", {});
34814
34954
  if (!json || json.result !== 0) {
34815
34955
  // An error occurred
34816
34956
  return;
@@ -34823,11 +34963,7 @@ class Model {
34823
34963
  async loadUserStats(userid, readyFunc) {
34824
34964
  // Load statistics for the given user
34825
34965
  try {
34826
- const json = await request({
34827
- method: "POST",
34828
- url: "/userstats",
34829
- body: { user: userid }
34830
- });
34966
+ const json = await this.post("/userstats", { user: userid });
34831
34967
  readyFunc(json);
34832
34968
  }
34833
34969
  catch (e) {
@@ -34837,13 +34973,7 @@ class Model {
34837
34973
  async loadPromoContent(key, readyFunc) {
34838
34974
  // Load HTML content for promo dialog
34839
34975
  try {
34840
- const html = await request({
34841
- method: "POST",
34842
- url: "/promo",
34843
- body: { key: key },
34844
- responseType: "text",
34845
- deserialize: (str) => str
34846
- });
34976
+ const html = await this.postText("/promo", { key: key });
34847
34977
  readyFunc(html);
34848
34978
  }
34849
34979
  catch (e) {
@@ -34892,11 +35022,7 @@ class Model {
34892
35022
  rack,
34893
35023
  limit: NUM_BEST_MOVES,
34894
35024
  };
34895
- const json = await requestMoves({
34896
- method: "POST",
34897
- url: "/moves",
34898
- body: rq,
34899
- });
35025
+ const json = await this.postMoves(rq);
34900
35026
  this.highlightedMove = null;
34901
35027
  if (!json || json.moves === undefined) {
34902
35028
  // Something unexpected going on
@@ -34931,12 +35057,7 @@ class Model {
34931
35057
  return; // Already loaded
34932
35058
  try {
34933
35059
  const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
34934
- const result = await request({
34935
- method: "GET",
34936
- url: "/rawhelp?version=malstadur&locale=" + locale,
34937
- responseType: "text",
34938
- deserialize: (str) => str
34939
- });
35060
+ const result = await this.getText("/rawhelp?version=malstadur&locale=" + locale);
34940
35061
  this.helpHTML = result;
34941
35062
  }
34942
35063
  catch (e) {
@@ -34951,12 +35072,7 @@ class Model {
34951
35072
  return; // Already loaded
34952
35073
  try {
34953
35074
  const locale = ((_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
34954
- const result = await request({
34955
- method: "GET",
34956
- url: "/friend?locale=" + locale,
34957
- responseType: "text",
34958
- deserialize: (str) => str
34959
- });
35075
+ const result = await this.getText("/friend?locale=" + locale);
34960
35076
  this.friendHTML = result;
34961
35077
  }
34962
35078
  catch (e) {
@@ -34972,10 +35088,7 @@ class Model {
34972
35088
  this.spinners++;
34973
35089
  }
34974
35090
  try {
34975
- const result = await request({
34976
- method: "POST",
34977
- url: "/loaduserprefs",
34978
- });
35091
+ const result = await this.post("/loaduserprefs");
34979
35092
  if (!result || !result.ok) {
34980
35093
  this.user = null;
34981
35094
  this.userErrors = null;
@@ -35002,11 +35115,7 @@ class Model {
35002
35115
  if (!user)
35003
35116
  return;
35004
35117
  try {
35005
- const result = await request({
35006
- method: "POST",
35007
- url: "/saveuserprefs",
35008
- body: user
35009
- });
35118
+ const result = await this.post("/saveuserprefs", user);
35010
35119
  if (result === null || result === void 0 ? void 0 : result.ok) {
35011
35120
  // User preferences modified successfully on the server:
35012
35121
  // update the state variables that we're caching
@@ -35420,8 +35529,10 @@ class Actions {
35420
35529
  }
35421
35530
  async markFavorite(userId, status) {
35422
35531
  // Mark or de-mark a user as a favorite
35532
+ if (!this.model.state)
35533
+ return;
35423
35534
  try {
35424
- await request({
35535
+ await request(this.model.state, {
35425
35536
  method: "POST",
35426
35537
  url: "/favorite",
35427
35538
  body: { destuser: userId, action: status ? "add" : "delete" }
@@ -35439,8 +35550,10 @@ class Actions {
35439
35550
  async handleChallenge(parameters) {
35440
35551
  var _a;
35441
35552
  // Reject or retract a challenge
35553
+ if (!this.model.state)
35554
+ return;
35442
35555
  try {
35443
- const json = await request({
35556
+ const json = await request(this.model.state, {
35444
35557
  method: "POST",
35445
35558
  url: "/challenge",
35446
35559
  body: parameters
@@ -35495,6 +35608,8 @@ class Actions {
35495
35608
  async startNewGame(oppid, reverse = false) {
35496
35609
  var _a;
35497
35610
  // Ask the server to initiate a new game against the given opponent
35611
+ if (!this.model.state)
35612
+ return;
35498
35613
  try {
35499
35614
  const rqBody = { opp: oppid, rev: reverse };
35500
35615
  if (this.model.isExplo) {
@@ -35507,7 +35622,7 @@ class Actions {
35507
35622
  url: "/initgame",
35508
35623
  body: rqBody
35509
35624
  };
35510
- const json = await request(rq);
35625
+ const json = await request(this.model.state, rq);
35511
35626
  if (json === null || json === void 0 ? void 0 : json.ok) {
35512
35627
  // Log the new game event
35513
35628
  const locale = ((_a = this.model.state) === null || _a === void 0 ? void 0 : _a.locale) || "is_IS";
@@ -35573,8 +35688,10 @@ class Actions {
35573
35688
  // User Preference Management Actions
35574
35689
  async setUserPref(pref) {
35575
35690
  // Set a user preference on the server
35691
+ if (!this.model.state)
35692
+ return;
35576
35693
  try {
35577
- await request({
35694
+ await request(this.model.state, {
35578
35695
  method: "POST",
35579
35696
  url: "/setuserpref",
35580
35697
  body: pref
@@ -35591,7 +35708,7 @@ class Actions {
35591
35708
  if (!user || !state)
35592
35709
  return false;
35593
35710
  try {
35594
- const json = await request({
35711
+ const json = await request(state, {
35595
35712
  method: "POST",
35596
35713
  url: "/cancelplan",
35597
35714
  body: {}
@@ -35709,41 +35826,25 @@ async function main$1(state, container) {
35709
35826
  console.error("No container element found");
35710
35827
  return "error";
35711
35828
  }
35712
- // Set up Netskrafl backend server URLs
35713
- setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
35714
35829
  try {
35715
- const loginData = await loginUserByEmail(state.userEmail, state.userNick, state.userFullname, state.token);
35716
- if (loginData.status === "expired") {
35717
- // The current Málstaður JWT has expired;
35718
- // we need to obtain a new one
35719
- return "expired";
35720
- }
35721
- if (loginData.status === "success") {
35722
- state.userId = loginData.user_id;
35723
- // Use the nickname from the server, if available
35724
- state.userNick = loginData.nickname || state.userNick;
35725
- // Log in to Firebase with the token passed from the server
35726
- await loginFirebase(state, loginData.firebase_token);
35727
- // Everything looks OK:
35728
- // Create the model, actions and view objects in proper sequence
35729
- const settings = getSettings();
35730
- const model = new Model(settings, state);
35731
- const actions = new Actions(model);
35732
- const view = new View(actions);
35733
- // Run the Mithril router
35734
- const routeResolver = createRouteResolver(actions, view);
35735
- m.route(container, settings.defaultRoute, routeResolver);
35736
- return "success";
35737
- }
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);
35738
35839
  }
35739
35840
  catch (e) {
35740
- console.error("Exception during login: ", e);
35841
+ console.error("Exception during initialization: ", e);
35842
+ return "error";
35741
35843
  }
35742
- m.mount(container, LoginError);
35743
- return "error";
35844
+ return "success";
35744
35845
  }
35745
35846
 
35746
- const mountForUser$1 = async (state, tokenExpired) => {
35847
+ const mountForUser$1 = async (state) => {
35747
35848
  // Return a DOM tree containing a mounted Netskrafl UI
35748
35849
  // for the user specified in the state object
35749
35850
  const { userEmail } = state;
@@ -35771,18 +35872,12 @@ const mountForUser$1 = async (state, tokenExpired) => {
35771
35872
  root.className = "netskrafl-user";
35772
35873
  return root;
35773
35874
  }
35774
- else if (loginResult === "expired") {
35775
- // We need a new token from the Málstaður backend
35776
- root.className = "netskrafl-expired";
35777
- tokenExpired && tokenExpired(); // This causes a reload of the component
35778
- return root;
35779
- }
35780
35875
  // console.error("Failed to mount Netskrafl UI for user", userEmail);
35781
35876
  throw new Error("Failed to mount Netskrafl UI");
35782
35877
  };
35783
35878
  const NetskraflImpl = ({ state, tokenExpired }) => {
35784
35879
  const ref = React.createRef();
35785
- const completeState = { ...DEFAULT_STATE, ...state };
35880
+ const completeState = makeGlobalState({ ...state, tokenExpired });
35786
35881
  const { userEmail } = completeState;
35787
35882
  /*
35788
35883
  useEffect(() => {
@@ -35815,7 +35910,7 @@ const NetskraflImpl = ({ state, tokenExpired }) => {
35815
35910
  return;
35816
35911
  }
35817
35912
  try {
35818
- mountForUser$1(completeState, tokenExpired).then((div) => {
35913
+ mountForUser$1(completeState).then((div) => {
35819
35914
  // Attach the div as a child of the container
35820
35915
  // instead of any previous children
35821
35916
  const container = ref.current;
@@ -36375,44 +36470,28 @@ async function main(state, container) {
36375
36470
  console.error("No container element found");
36376
36471
  return "error";
36377
36472
  }
36378
- // Set up Netskrafl backend server URLs
36379
- setServerUrl(state.serverUrl, state.movesUrl, state.movesAccessKey);
36380
36473
  try {
36381
- const loginData = await loginUserByEmail(state.userEmail, state.userNick, state.userFullname, state.token);
36382
- if (loginData.status === "expired") {
36383
- // The current Málstaður JWT has expired;
36384
- // we need to obtain a new one
36385
- return "expired";
36386
- }
36387
- if (loginData.status === "success") {
36388
- state.userId = loginData.user_id;
36389
- // Use the nickname from the server, if available
36390
- state.userNick = loginData.nickname || state.userNick;
36391
- // Log in to Firebase with the token passed from the server
36392
- await loginFirebase(state, loginData.firebase_token);
36393
- // Everything looks OK:
36394
- // Create the model, view and actions objects
36395
- const settings = getSettings();
36396
- const model = new Model(settings, state);
36397
- const actions = new Actions(model);
36398
- const view = new View(actions);
36399
- const today = new Date().toISOString().split("T")[0];
36400
- const locale = state.locale || "is_IS";
36401
- // Mount the Gáta Dagsins UI using an anonymous closure component
36402
- m.mount(container, {
36403
- view: () => m(GataDagsins$1, { view, date: today, locale }),
36404
- });
36405
- return "success";
36406
- }
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
+ });
36407
36486
  }
36408
36487
  catch (e) {
36409
- console.error("Exception during login: ", e);
36488
+ console.error("Exception during initialization: ", e);
36489
+ return "error";
36410
36490
  }
36411
- m.mount(container, LoginError);
36412
- return "error";
36491
+ return "success";
36413
36492
  }
36414
36493
 
36415
- const mountForUser = async (state, tokenExpired) => {
36494
+ const mountForUser = async (state) => {
36416
36495
  // Return a DOM tree containing a mounted Gáta Dagsins UI
36417
36496
  // for the user specified in the state object
36418
36497
  const { userEmail } = state;
@@ -36440,18 +36519,12 @@ const mountForUser = async (state, tokenExpired) => {
36440
36519
  root.className = "gatadagsins-user";
36441
36520
  return root;
36442
36521
  }
36443
- else if (loginResult === "expired") {
36444
- // We need a new token from the Málstaður backend
36445
- root.className = "gatadagsins-expired";
36446
- tokenExpired && tokenExpired(); // This causes a reload of the component
36447
- return root;
36448
- }
36449
36522
  // console.error("Failed to mount Gáta Dagsins UI for user", userEmail);
36450
36523
  throw new Error("Failed to mount Gáta Dagsins UI");
36451
36524
  };
36452
36525
  const GataDagsinsImpl = ({ state, tokenExpired }) => {
36453
36526
  const ref = React.createRef();
36454
- const completeState = { ...DEFAULT_STATE, ...state };
36527
+ const completeState = makeGlobalState({ ...state, tokenExpired });
36455
36528
  const { userEmail } = completeState;
36456
36529
  useEffect(() => {
36457
36530
  var _a;
@@ -36469,7 +36542,7 @@ const GataDagsinsImpl = ({ state, tokenExpired }) => {
36469
36542
  return;
36470
36543
  }
36471
36544
  try {
36472
- mountForUser(completeState, tokenExpired).then((div) => {
36545
+ mountForUser(completeState).then((div) => {
36473
36546
  // Attach the div as a child of the container
36474
36547
  // instead of any previous children
36475
36548
  const container = ref.current;