@mideind/netskrafl-react 1.7.0 → 1.7.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/cjs/index.js CHANGED
@@ -28,6 +28,8 @@ const DEFAULT_STATE = {
28
28
  fairPlay: false,
29
29
  plan: "", // Not a friend
30
30
  hasPaid: false,
31
+ audio: false,
32
+ fanfare: false,
31
33
  ready: true,
32
34
  readyTimed: true,
33
35
  uiFullscreen: true,
@@ -88,6 +90,10 @@ const saveAuthSettings = (settings) => {
88
90
  filteredSettings.ready = settings.ready;
89
91
  if (settings.readyTimed !== undefined)
90
92
  filteredSettings.readyTimed = settings.readyTimed;
93
+ if (settings.audio !== undefined)
94
+ filteredSettings.audio = settings.audio;
95
+ if (settings.fanfare !== undefined)
96
+ filteredSettings.fanfare = settings.fanfare;
91
97
  // Only save if we have actual settings to persist
92
98
  if (Object.keys(filteredSettings).length > 1) {
93
99
  sessionStorage.setItem(AUTH_SETTINGS_KEY, JSON.stringify(filteredSettings));
@@ -126,7 +132,7 @@ const clearAuthSettings = () => {
126
132
  };
127
133
  // Apply persisted settings to a GlobalState object
128
134
  const applyPersistedSettings = (state) => {
129
- var _a, _b, _c, _d;
135
+ var _a, _b, _c, _d, _e, _f;
130
136
  const persisted = loadAuthSettings();
131
137
  if (!persisted) {
132
138
  return state;
@@ -152,6 +158,8 @@ const applyPersistedSettings = (state) => {
152
158
  fairPlay: (_b = persisted.fairPlay) !== null && _b !== void 0 ? _b : state.fairPlay,
153
159
  ready: (_c = persisted.ready) !== null && _c !== void 0 ? _c : state.ready,
154
160
  readyTimed: (_d = persisted.readyTimed) !== null && _d !== void 0 ? _d : state.readyTimed,
161
+ audio: (_e = persisted.audio) !== null && _e !== void 0 ? _e : state.audio,
162
+ fanfare: (_f = persisted.fanfare) !== null && _f !== void 0 ? _f : state.fanfare,
155
163
  };
156
164
  };
157
165
 
@@ -27358,6 +27366,19 @@ let app;
27358
27366
  let auth;
27359
27367
  let database;
27360
27368
  let analytics;
27369
+ // Queue of callbacks to execute when database is ready
27370
+ let databaseReadyQueue = [];
27371
+ /**
27372
+ * Execute all queued callbacks now that database is ready
27373
+ */
27374
+ function flushDatabaseReadyQueue(state) {
27375
+ var _a;
27376
+ const queue = [...databaseReadyQueue];
27377
+ databaseReadyQueue = []; // Clear queue before executing to avoid re-queuing
27378
+ queue.forEach(callback => callback());
27379
+ // Also notify the global callback if set
27380
+ (_a = state.onFirebaseReady) === null || _a === void 0 ? void 0 : _a.call(state);
27381
+ }
27361
27382
  function initFirebase(state) {
27362
27383
  try {
27363
27384
  const { projectId, firebaseApiKey, databaseUrl, firebaseSenderId, firebaseAppId, measurementId } = state;
@@ -27392,9 +27413,30 @@ function isFirebaseAuthenticated(state) {
27392
27413
  if (app)
27393
27414
  auth = getAuth(app);
27394
27415
  }
27395
- if (!auth)
27416
+ if (!auth || auth.currentUser === null) {
27396
27417
  return false;
27397
- return auth.currentUser !== null;
27418
+ }
27419
+ // If auth is valid but database not initialized, initialize it
27420
+ // This handles the case where the user has cached auth but returns
27421
+ // directly to a route that needs Firebase Database (e.g., Gáta Dagsins)
27422
+ if (!database && app) {
27423
+ database = getDatabase(app);
27424
+ if (!database) {
27425
+ console.error("Failed to initialize Firebase Database");
27426
+ return false;
27427
+ }
27428
+ // Database initialized successfully - flush queued callbacks
27429
+ flushDatabaseReadyQueue(state);
27430
+ // Also initialize analytics if not already done
27431
+ if (!analytics) {
27432
+ analytics = getAnalytics(app);
27433
+ if (!analytics) {
27434
+ console.error("Failed to initialize Firebase Analytics");
27435
+ // We don't return false here since analytics is not critical
27436
+ }
27437
+ }
27438
+ }
27439
+ return true;
27398
27440
  }
27399
27441
  async function loginFirebase(state, firebaseToken, onLoginFunc) {
27400
27442
  if (!app && !initFirebase(state))
@@ -27432,6 +27474,10 @@ async function loginFirebase(state, firebaseToken, onLoginFunc) {
27432
27474
  if (!database) {
27433
27475
  console.error("Failed to initialize Firebase Database");
27434
27476
  }
27477
+ else {
27478
+ // Database initialized successfully - flush queued callbacks
27479
+ flushDatabaseReadyQueue(state);
27480
+ }
27435
27481
  analytics = getAnalytics(app);
27436
27482
  if (!analytics) {
27437
27483
  console.error("Failed to initialize Firebase Analytics");
@@ -27463,8 +27509,12 @@ function initPresence(projectId, userId, locale) {
27463
27509
  }
27464
27510
  function attachFirebaseListener(path, func) {
27465
27511
  // Attach a message listener to a Firebase path
27466
- if (!database)
27512
+ // console.log(`attachFirebaseListener(${path})`);
27513
+ if (!database) {
27514
+ // Database not ready yet - queue this attachment for later
27515
+ databaseReadyQueue.push(() => attachFirebaseListener(path, func));
27467
27516
  return;
27517
+ }
27468
27518
  let cnt = 0;
27469
27519
  const pathRef = ref(database, path);
27470
27520
  onValue(pathRef, function (snapshot) {
@@ -27481,8 +27531,10 @@ function attachFirebaseListener(path, func) {
27481
27531
  }
27482
27532
  function detachFirebaseListener(path) {
27483
27533
  // Detach a message listener from a Firebase path
27484
- if (!database)
27534
+ // console.log(`detachFirebaseListener(${path})`);
27535
+ if (!database) {
27485
27536
  return;
27537
+ }
27486
27538
  const pathRef = ref(database, path);
27487
27539
  off(pathRef);
27488
27540
  }
@@ -27501,63 +27553,178 @@ async function getFirebaseData(path) {
27501
27553
  return snapshot.val();
27502
27554
  }
27503
27555
 
27504
- // Global state for authentication
27556
+ // ============================================================================
27557
+ // Configuration constants for login protection
27558
+ // ============================================================================
27559
+ // Maximum number of login attempts before giving up
27560
+ const MAX_LOGIN_RETRIES = 3;
27561
+ // Minimum time between login attempts (milliseconds) - prevents rapid-fire retries
27562
+ const MIN_LOGIN_INTERVAL_MS = 500;
27563
+ // Initial backoff delay for retries (milliseconds) - doubles each retry
27564
+ const INITIAL_BACKOFF_MS = 500;
27565
+ // Maximum backoff delay (milliseconds) - caps exponential growth
27566
+ const MAX_BACKOFF_MS = 5000;
27567
+ // Circuit breaker timeout (milliseconds) - how long to wait after max retries
27568
+ const CIRCUIT_BREAKER_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
27569
+ // ============================================================================
27570
+ // Global state for authentication and retry protection
27571
+ // ============================================================================
27572
+ // Active login promise - prevents concurrent login attempts
27505
27573
  let authPromise = null;
27506
- // Custom error class for authentication failures
27574
+ // Retry tracking
27575
+ let loginAttemptCount = 0;
27576
+ let lastLoginAttemptTime = 0;
27577
+ // Circuit breaker state
27578
+ let circuitBreakerOpen = false;
27579
+ let circuitBreakerResetTime = 0;
27580
+ // ============================================================================
27581
+ // Custom error classes
27582
+ // ============================================================================
27507
27583
  class AuthenticationError extends Error {
27508
27584
  constructor() {
27509
27585
  super("Authentication required");
27510
27586
  this.name = "AuthenticationError";
27511
27587
  }
27512
27588
  }
27513
- // Internal function to ensure authentication
27589
+ class LoginThrottledError extends Error {
27590
+ constructor(message) {
27591
+ super(message);
27592
+ this.name = "LoginThrottledError";
27593
+ }
27594
+ }
27595
+ // ============================================================================
27596
+ // Helper functions
27597
+ // ============================================================================
27598
+ /**
27599
+ * Delays execution for the specified number of milliseconds
27600
+ */
27601
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
27602
+ /**
27603
+ * Calculates exponential backoff delay based on attempt number
27604
+ */
27605
+ const calculateBackoff = (attemptNumber) => {
27606
+ const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attemptNumber - 1);
27607
+ return Math.min(backoff, MAX_BACKOFF_MS);
27608
+ };
27609
+ /**
27610
+ * Resets all retry and circuit breaker state to initial values
27611
+ */
27612
+ const resetLoginState = () => {
27613
+ loginAttemptCount = 0;
27614
+ circuitBreakerOpen = false;
27615
+ circuitBreakerResetTime = 0;
27616
+ };
27617
+ /**
27618
+ * Opens the circuit breaker to prevent further login attempts
27619
+ */
27620
+ const openCircuitBreaker = () => {
27621
+ circuitBreakerOpen = true;
27622
+ circuitBreakerResetTime = Date.now() + CIRCUIT_BREAKER_TIMEOUT_MS;
27623
+ loginAttemptCount = 0;
27624
+ console.error(`Circuit breaker opened. Login attempts blocked until ${new Date(circuitBreakerResetTime).toLocaleTimeString()}`);
27625
+ };
27626
+ /**
27627
+ * Checks if circuit breaker should be reset based on timeout
27628
+ */
27629
+ const checkCircuitBreakerReset = () => {
27630
+ if (circuitBreakerOpen && Date.now() >= circuitBreakerResetTime) {
27631
+ resetLoginState();
27632
+ }
27633
+ };
27634
+ // ============================================================================
27635
+ // Authentication flow
27636
+ // ============================================================================
27637
+ /**
27638
+ * Internal function to ensure authentication with comprehensive retry protection
27639
+ *
27640
+ * Protection mechanisms:
27641
+ * - Circuit breaker: Stops all attempts after max retries for a cooldown period
27642
+ * - Retry limits: Maximum number of consecutive failed attempts
27643
+ * - Exponential backoff: Increasing delays between retries
27644
+ * - Rate limiting: Minimum time between attempts
27645
+ * - Request deduplication: Only one login attempt at a time
27646
+ */
27514
27647
  const ensureAuthenticated = async (state) => {
27515
- var _a, _b, _c, _d, _e, _f, _g, _h;
27648
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
27649
+ // Check and potentially reset circuit breaker
27650
+ checkCircuitBreakerReset();
27651
+ // Circuit breaker check - fail fast if too many recent failures
27652
+ if (circuitBreakerOpen) {
27653
+ const waitMinutes = Math.ceil((circuitBreakerResetTime - Date.now()) / 60000);
27654
+ throw new LoginThrottledError(`Too many failed login attempts. Please wait ${waitMinutes} minute(s) before trying again.`);
27655
+ }
27516
27656
  // If login is already in progress, wait for it to complete
27517
27657
  if (authPromise) {
27518
27658
  await authPromise;
27519
27659
  return;
27520
27660
  }
27661
+ // Rate limiting - prevent rapid-fire retries
27662
+ const timeSinceLastAttempt = Date.now() - lastLoginAttemptTime;
27663
+ if (timeSinceLastAttempt < MIN_LOGIN_INTERVAL_MS && loginAttemptCount > 0) {
27664
+ console.warn(`Login attempt too soon (${timeSinceLastAttempt}ms since last attempt)`);
27665
+ throw new LoginThrottledError("Please wait a moment before trying again.");
27666
+ }
27667
+ // Retry loop with exponential backoff
27521
27668
  let continueTrying = true;
27522
- while (continueTrying) {
27669
+ while (continueTrying && loginAttemptCount < MAX_LOGIN_RETRIES) {
27523
27670
  continueTrying = false;
27524
- // Start new login attempt (either forced by 401 or needed for Firebase)
27671
+ loginAttemptCount++;
27672
+ lastLoginAttemptTime = Date.now();
27673
+ // Apply exponential backoff for retries (skip delay on first attempt)
27674
+ if (loginAttemptCount >= 2) {
27675
+ const backoffMs = calculateBackoff(loginAttemptCount - 1);
27676
+ console.warn(`Login retry ${loginAttemptCount}/${MAX_LOGIN_RETRIES}, waiting ${backoffMs}ms...`);
27677
+ await delay(backoffMs);
27678
+ }
27679
+ // Start new login attempt
27525
27680
  authPromise = loginUserByEmail(state);
27526
27681
  try {
27527
27682
  const result = await authPromise;
27683
+ // Handle expired token
27528
27684
  if (result.status === "expired") {
27529
- // Token has expired, notify the React component if callback is set
27530
27685
  if (state.tokenExpired) {
27531
- // We have a callback to renew the token: do it and try again
27532
- state.tokenExpired();
27533
- continueTrying = true; // Try logging in again
27534
- clearAuthSettings();
27535
- continue;
27686
+ // Token refresh callback is available
27687
+ try {
27688
+ // Call the token refresh callback
27689
+ state.tokenExpired();
27690
+ continueTrying = true;
27691
+ clearAuthSettings();
27692
+ continue;
27693
+ }
27694
+ catch (error) {
27695
+ console.error("Token refresh failed:", error);
27696
+ clearAuthSettings();
27697
+ throw new Error(`Failed to refresh authentication token: ${error instanceof Error ? error.message : String(error)}`);
27698
+ }
27536
27699
  }
27537
- // Clear any persisted settings since they're no longer valid
27700
+ // No refresh callback available
27538
27701
  clearAuthSettings();
27539
- throw new Error("Authentication token has expired");
27702
+ throw new Error("Authentication token has expired. Please log in again.");
27540
27703
  }
27541
- else if (result.status !== "success") {
27542
- // Clear any persisted settings on auth failure
27704
+ // Handle other non-success statuses
27705
+ if (result.status !== "success") {
27543
27706
  clearAuthSettings();
27544
27707
  throw new Error(`Authentication failed: ${result.message || result.status}`);
27545
27708
  }
27709
+ // ========================================================================
27710
+ // Success! Update global state and persist authentication
27711
+ // ========================================================================
27546
27712
  // Update the user's ID to the internal one used by the backend and Firebase
27547
27713
  state.userId = result.user_id || state.userId;
27548
27714
  state.account = result.account || state.userId;
27549
- // Update the user's nickname
27550
27715
  state.userNick = result.nickname || state.userNick;
27551
- // Use the server's Firebase API key, if provided
27552
27716
  state.firebaseApiKey = result.firebase_api_key || state.firebaseApiKey;
27553
27717
  // Load state flags and preferences
27554
27718
  state.beginner = (_b = (_a = result.prefs) === null || _a === void 0 ? void 0 : _a.beginner) !== null && _b !== void 0 ? _b : true;
27555
27719
  state.fairPlay = (_d = (_c = result.prefs) === null || _c === void 0 ? void 0 : _c.fairplay) !== null && _d !== void 0 ? _d : false;
27556
27720
  state.ready = (_f = (_e = result.prefs) === null || _e === void 0 ? void 0 : _e.ready) !== null && _f !== void 0 ? _f : true;
27557
27721
  state.readyTimed = (_h = (_g = result.prefs) === null || _g === void 0 ? void 0 : _g.ready_timed) !== null && _h !== void 0 ? _h : true;
27558
- // Save the authentication settings to sessionStorage for persistence
27722
+ state.audio = (_k = (_j = result.prefs) === null || _j === void 0 ? void 0 : _j.audio) !== null && _k !== void 0 ? _k : false;
27723
+ state.fanfare = (_m = (_l = result.prefs) === null || _l === void 0 ? void 0 : _l.fanfare) !== null && _m !== void 0 ? _m : false;
27724
+ state.hasPaid = (_p = (_o = result.prefs) === null || _o === void 0 ? void 0 : _o.haspaid) !== null && _p !== void 0 ? _p : false;
27725
+ // Save authentication settings to sessionStorage
27559
27726
  saveAuthSettings({
27560
- userEmail: state.userEmail, // CRITICAL: Include email to validate ownership
27727
+ userEmail: state.userEmail,
27561
27728
  userId: state.userId,
27562
27729
  userNick: state.userNick,
27563
27730
  firebaseApiKey: state.firebaseApiKey,
@@ -27565,18 +27732,39 @@ const ensureAuthenticated = async (state) => {
27565
27732
  fairPlay: state.fairPlay,
27566
27733
  ready: state.ready,
27567
27734
  readyTimed: state.readyTimed,
27735
+ audio: state.audio,
27736
+ fanfare: state.fanfare,
27568
27737
  });
27569
- // Be sure to redraw Mithril components after authentication;
27570
- // user info may have changed
27738
+ // Redraw UI to reflect authentication changes
27571
27739
  m.redraw();
27572
- // Success: Log in to Firebase with the token passed from the server
27740
+ // Log in to Firebase with the token from server
27573
27741
  await loginFirebase(state, result.firebase_token);
27742
+ // Success - reset all retry state
27743
+ resetLoginState();
27744
+ }
27745
+ catch (error) {
27746
+ // On last retry, open circuit breaker
27747
+ if (loginAttemptCount >= MAX_LOGIN_RETRIES) {
27748
+ openCircuitBreaker();
27749
+ throw new Error(`Login failed after ${MAX_LOGIN_RETRIES} attempts. ` +
27750
+ `Please wait ${Math.ceil(CIRCUIT_BREAKER_TIMEOUT_MS / 60000)} minutes before trying again.`);
27751
+ }
27752
+ // Re-throw error if we're not retrying
27753
+ if (!continueTrying) {
27754
+ throw error;
27755
+ }
27756
+ // Continue to next retry iteration
27574
27757
  }
27575
27758
  finally {
27576
- // Reset the promise so future 401s can trigger a new login
27759
+ // Reset the promise so future attempts can proceed
27577
27760
  authPromise = null;
27578
27761
  }
27579
27762
  }
27763
+ // Should not reach here, but handle max retries edge case
27764
+ if (loginAttemptCount >= MAX_LOGIN_RETRIES) {
27765
+ openCircuitBreaker();
27766
+ throw new Error("Maximum login attempts exceeded. Please try again later.");
27767
+ }
27580
27768
  };
27581
27769
  // Internal authenticated request function
27582
27770
  const authenticatedRequest = async (state, options, retries = 0) => {
@@ -27824,43 +28012,49 @@ const UserInfoButton = {
27824
28012
  }, isRobot ? "" : m("span.usr-info"));
27825
28013
  }
27826
28014
  };
27827
- const OnlinePresence = (initialVnode) => {
28015
+ const OnlinePresence = {
27828
28016
  // Shows an icon in grey or green depending on whether a given user
27829
28017
  // is online or not. If attrs.online is given (i.e. not undefined),
27830
28018
  // that value is used and displayed; otherwise the server is asked.
27831
- const attrs = initialVnode.attrs;
27832
- let online = attrs.online ? true : false;
27833
- const askServer = attrs.online === undefined;
27834
- const id = attrs.id;
27835
- const userId = attrs.userId;
27836
- const state = attrs.state;
27837
- let loading = false;
27838
- async function _update() {
27839
- if (askServer && !loading) {
27840
- loading = true;
27841
- const json = await request(state, {
27842
- method: "POST",
27843
- url: "/onlinecheck",
27844
- body: { user: userId }
27845
- });
27846
- online = json && json.online;
27847
- loading = false;
28019
+ oninit: (vnode) => {
28020
+ const { state } = vnode;
28021
+ state.online = false;
28022
+ },
28023
+ oncreate: (vnode) => {
28024
+ // Note: we must use a two-step initialization here,
28025
+ // calling oninit() first and then oncreate(), since calls to
28026
+ // m.redraw() - explicit or implicit via m.request() - from within
28027
+ // oninit() will cause a recursive call to oninit(), resulting in
28028
+ // a double API call to the backend.
28029
+ const { attrs, state } = vnode;
28030
+ state.online = !!attrs.online;
28031
+ if (attrs.online === undefined) {
28032
+ // We need to ask the server for the online status
28033
+ const askServer = async () => {
28034
+ try {
28035
+ const json = await request(attrs.state, {
28036
+ method: "POST",
28037
+ url: "/onlinecheck",
28038
+ body: { user: attrs.userId }
28039
+ });
28040
+ state.online = json && json.online;
28041
+ }
28042
+ catch (e) {
28043
+ state.online = false;
28044
+ }
28045
+ };
28046
+ askServer(); // Fire-and-forget
27848
28047
  }
28048
+ },
28049
+ view: (vnode) => {
28050
+ const { attrs, state } = vnode;
28051
+ const online = state.online || (!!attrs.online);
28052
+ return m("span", {
28053
+ id: attrs.id,
28054
+ title: online ? ts("Er álínis") : ts("Álínis?"),
28055
+ class: online ? "online" : ""
28056
+ });
27849
28057
  }
27850
- return {
27851
- oninit: _update,
27852
- view: (vnode) => {
27853
- var _a, _b;
27854
- if (!askServer)
27855
- // Display the state of the online attribute as-is
27856
- online = (_b = (_a = vnode.attrs) === null || _a === void 0 ? void 0 : _a.online) !== null && _b !== void 0 ? _b : false;
27857
- return m("span", {
27858
- id: id,
27859
- title: online ? ts("Er álínis") : ts("Álínis?"),
27860
- class: online ? "online" : ""
27861
- });
27862
- }
27863
- };
27864
28058
  };
27865
28059
  const UserId = {
27866
28060
  // User identifier at top right, opens user preferences
@@ -28115,146 +28309,148 @@ const TogglerFairplay = () => {
28115
28309
  For further information, see https://github.com/mideind/Netskrafl
28116
28310
 
28117
28311
  */
28118
- const WaitDialog = (initialVnode) => {
28312
+ const WaitDialog = {
28119
28313
  // A dialog that is shown while the user waits for the opponent,
28120
28314
  // who issued a timed game challenge, to be ready
28121
- var _a, _b;
28122
- const attrs = initialVnode.attrs;
28123
- const view = attrs.view;
28124
- const model = view.model;
28125
- const state = model.state;
28126
- const duration = attrs.duration;
28127
- const oppId = attrs.oppId;
28128
- const key = attrs.challengeKey;
28129
- let oppNick = attrs.oppNick;
28130
- let oppName = attrs.oppName;
28131
- let oppOnline = false;
28132
- const userId = (_b = (_a = model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "";
28133
- // Firebase path
28134
- const path = 'user/' + userId + "/wait/" + oppId;
28135
- // Flag set when the new game has been initiated
28136
- let pointOfNoReturn = false;
28137
- let initialized = false;
28138
- async function updateOnline() {
28139
- // Initiate an online check on the opponent
28140
- try {
28141
- if (!oppId || !key || !state)
28142
- return;
28143
- const json = await request(state, {
28144
- method: "POST",
28145
- url: "/initwait",
28146
- body: { opp: oppId, key }
28147
- });
28148
- // If json.waiting is false, the initiation failed
28149
- // and there is really no point in continuing to wait
28150
- if (json && json.online && json.waiting)
28151
- // The user is online
28152
- oppOnline = true;
28153
- }
28154
- catch (e) {
28155
- }
28156
- }
28157
- async function cancelWait() {
28158
- // Cancel a pending wait for a timed game
28159
- if (!state)
28160
- return;
28161
- try {
28162
- await request(state, {
28163
- method: "POST",
28164
- url: "/cancelwait",
28165
- body: {
28166
- user: userId,
28167
- opp: oppId,
28168
- key
28169
- }
28170
- });
28171
- }
28172
- catch (e) {
28173
- }
28174
- }
28175
- const oncreate = async () => {
28176
- if (!userId || !oppId || initialized)
28315
+ oninit: (vnode) => {
28316
+ const { state } = vnode;
28317
+ state.oppOnline = false;
28318
+ state.pointOfNoReturn = false;
28319
+ state.firebasePath = "";
28320
+ },
28321
+ oncreate: (vnode) => {
28322
+ var _a, _b;
28323
+ // Note: we must use a two-step initialization here,
28324
+ // calling oninit() first and then oncreate(), since calls to
28325
+ // m.redraw() - explicit or implicit via m.request() - from within
28326
+ // oninit() will cause a recursive call to oninit(), resulting in
28327
+ // a double API call to the backend.
28328
+ const { attrs, state } = vnode;
28329
+ const { view, oppId, challengeKey: key } = attrs;
28330
+ const { model } = view;
28331
+ const globalState = model.state;
28332
+ const userId = (_b = (_a = model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "";
28333
+ if (!userId || !oppId)
28177
28334
  return; // Should not happen
28178
- initialized = true;
28179
- updateOnline();
28335
+ state.firebasePath = `user/${userId}/wait/${oppId}`;
28336
+ // Initiate an online check on the opponent (fire-and-forget)
28337
+ const updateOnline = async () => {
28338
+ try {
28339
+ if (key && globalState) {
28340
+ const json = await request(globalState, {
28341
+ method: "POST",
28342
+ url: "/initwait",
28343
+ body: { opp: oppId, key }
28344
+ });
28345
+ // If json.waiting is false, the initiation failed
28346
+ // and there is really no point in continuing to wait
28347
+ if (json && json.online && json.waiting) {
28348
+ // The user is online
28349
+ state.oppOnline = true;
28350
+ }
28351
+ }
28352
+ }
28353
+ catch (e) {
28354
+ }
28355
+ };
28356
+ updateOnline(); // Fire-and-forget, don't await
28180
28357
  // Attach a Firebase listener to the wait path
28181
- attachFirebaseListener(path, (json) => {
28358
+ attachFirebaseListener(state.firebasePath, (json) => {
28182
28359
  if (json !== true && json.game) {
28183
28360
  // A new game has been created and initiated by the server
28184
- pointOfNoReturn = true;
28185
- detachFirebaseListener(path);
28361
+ state.pointOfNoReturn = true;
28362
+ detachFirebaseListener(state.firebasePath);
28186
28363
  // We don't need to pop the dialog; that is done automatically
28187
28364
  // by the route resolver upon m.route.set()
28188
28365
  // Navigate to the newly initiated game
28189
28366
  m.route.set("/game/" + json.game);
28190
28367
  }
28191
28368
  });
28192
- };
28193
- return {
28194
- oncreate,
28195
- view: () => {
28196
- if (!state)
28197
- return null;
28198
- return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
28199
- m(".chall-hdr", m("table", m("tbody", m("tr", [
28200
- m("td", m("h1.chall-icon", glyph("time"))),
28201
- m("td.l-border", [
28202
- m(OnlinePresence, { id: "chall-online", userId: oppId, online: oppOnline, state }),
28203
- m("h1", oppNick),
28204
- m("h2", oppName)
28205
- ])
28206
- ])))),
28207
- m(".wait-explain", [
28208
- mt("p", [
28209
- "Þú ert reiðubúin(n) að taka áskorun um viðureign með klukku, ",
28210
- m("strong", ["2 x ", duration.toString(), ts(" mínútur.")])
28211
- ]),
28212
- mt("p", [
28213
- "Beðið er eftir að áskorandinn ", m("strong", oppNick),
28214
- " sé ", oppOnline ? "" : mt("span#chall-is-online", "álínis og "), "til í tuskið."
28215
- ]),
28216
- mt("p", "Leikur hefst um leið og áskorandinn bregst við. Handahóf ræður hvor byrjar."),
28217
- mt("p", "Ef þér leiðist biðin geturðu hætt við og reynt aftur síðar.")
28369
+ },
28370
+ view: (vnode) => {
28371
+ var _a, _b;
28372
+ const { attrs, state } = vnode;
28373
+ const { view, oppId, duration, challengeKey: key, oppNick, oppName } = attrs;
28374
+ const { model } = view;
28375
+ const globalState = model.state;
28376
+ const userId = (_b = (_a = model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "";
28377
+ if (!globalState)
28378
+ return null;
28379
+ const cancelWait = async () => {
28380
+ // Cancel a pending wait for a timed game
28381
+ if (!globalState)
28382
+ return;
28383
+ try {
28384
+ request(globalState, {
28385
+ method: "POST",
28386
+ url: "/cancelwait",
28387
+ body: {
28388
+ user: userId,
28389
+ opp: oppId,
28390
+ key
28391
+ }
28392
+ });
28393
+ }
28394
+ catch (e) {
28395
+ }
28396
+ };
28397
+ return m(".modal-dialog", { id: "wait-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { "id": "wait-form" }, [
28398
+ m(".chall-hdr", m("table", m("tbody", m("tr", [
28399
+ m("td", m("h1.chall-icon", glyph("time"))),
28400
+ m("td.l-border", [
28401
+ m(OnlinePresence, { id: "chall-online", userId: oppId, online: state.oppOnline, state: globalState }),
28402
+ m("h1", oppNick),
28403
+ m("h2", oppName),
28404
+ ])
28405
+ ])))),
28406
+ m(".wait-explain", [
28407
+ mt("p", [
28408
+ "Þú ert reiðubúin(n) að taka áskorun um viðureign með klukku,",
28218
28409
  ]),
28219
- m(DialogButton, {
28220
- id: "wait-cancel",
28221
- title: ts("Hætta við"),
28222
- onclick: (ev) => {
28223
- // Cancel the wait status and navigate back to the main page
28224
- if (pointOfNoReturn) {
28225
- // Actually, it's too late to cancel
28226
- ev.preventDefault();
28227
- return;
28228
- }
28229
- detachFirebaseListener(path);
28230
- cancelWait();
28231
- view.popDialog();
28410
+ m("p", [
28411
+ m("strong", ["2 x ", duration.toString(), ts(" mínútur.")])
28412
+ ]),
28413
+ mt("p", [
28414
+ "Beðið er eftir áskorandinn ", m("strong", oppNick),
28415
+ " sé ", state.oppOnline ? "" : mt("span#chall-is-online", "álínis og "), "til í tuskið."
28416
+ ]),
28417
+ mt("p", "Leikur hefst um leið og áskorandinn bregst við. Handahóf ræður hvor byrjar."),
28418
+ mt("p", "Ef þér leiðist biðin geturðu hætt við og reynt aftur síðar.")
28419
+ ]),
28420
+ m(DialogButton, {
28421
+ id: "wait-cancel",
28422
+ title: ts("Hætta við"),
28423
+ onclick: (ev) => {
28424
+ // Cancel the wait status and navigate back to the main page
28425
+ if (state.pointOfNoReturn) {
28426
+ // Actually, it's too late to cancel
28232
28427
  ev.preventDefault();
28428
+ return;
28233
28429
  }
28234
- }, glyph("remove"))
28235
- ]));
28236
- }
28237
- };
28430
+ if (state.firebasePath)
28431
+ detachFirebaseListener(state.firebasePath);
28432
+ cancelWait();
28433
+ view.popDialog();
28434
+ ev.preventDefault();
28435
+ }
28436
+ }, glyph("remove"))
28437
+ ]));
28438
+ }
28238
28439
  };
28239
- const AcceptDialog = (initialVnode) => {
28440
+ const AcceptDialog = {
28240
28441
  // A dialog that is shown (usually briefly) while
28241
28442
  // the user who originated a timed game challenge
28242
28443
  // is linked up with her opponent and a new game is started
28243
- const attrs = initialVnode.attrs;
28244
- const view = attrs.view;
28245
- const state = view.model.state;
28246
- const oppId = attrs.oppId;
28247
- const key = attrs.challengeKey;
28248
- let oppNick = attrs.oppNick;
28249
- let oppReady = true;
28250
- let loading = false;
28251
- async function waitCheck() {
28444
+ oninit: async (vnode) => {
28445
+ const { attrs, state } = vnode;
28446
+ const { view, oppId, challengeKey: key } = attrs;
28447
+ const globalState = view.model.state;
28448
+ if (!globalState)
28449
+ return; // Should not happen
28450
+ state.oppReady = true;
28252
28451
  // Initiate a wait status check on the opponent
28253
- if (loading || !state)
28254
- return; // Already checking
28255
- loading = true;
28256
28452
  try {
28257
- const json = await request(state, {
28453
+ const json = await request(globalState, {
28258
28454
  method: "POST",
28259
28455
  url: "/waitcheck",
28260
28456
  body: { user: oppId, key }
@@ -28265,42 +28461,40 @@ const AcceptDialog = (initialVnode) => {
28265
28461
  // and all open dialogs are thereby closed automatically.
28266
28462
  view.actions.startNewGame(oppId, true);
28267
28463
  }
28268
- else
28464
+ else {
28269
28465
  // Something didn't check out: keep the dialog open
28270
28466
  // until the user manually closes it
28271
- oppReady = false;
28467
+ state.oppReady = false;
28468
+ }
28272
28469
  }
28273
28470
  catch (e) {
28471
+ state.oppReady = false;
28274
28472
  }
28275
- finally {
28276
- loading = false;
28277
- }
28473
+ },
28474
+ view: (vnode) => {
28475
+ const { attrs, state } = vnode;
28476
+ const { view, oppNick } = attrs;
28477
+ return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
28478
+ m(".chall-hdr", m("table", m("tbody", m("tr", [
28479
+ m("td", m("h1.chall-icon", glyph("time"))),
28480
+ m("td.l-border", m("h1", oppNick))
28481
+ ])))),
28482
+ m("div", { "style": { "text-align": "center", "padding-top": "32px" } }, [
28483
+ m("p", mt("strong", "Viðureign með klukku")),
28484
+ mt("p", state.oppReady ? "Athuga hvort andstæðingur er reiðubúinn..."
28485
+ : ["Andstæðingurinn ", m("strong", oppNick), " er ekki reiðubúinn"])
28486
+ ]),
28487
+ m(DialogButton, {
28488
+ id: 'accept-cancel',
28489
+ title: ts('Reyna síðar'),
28490
+ onclick: (ev) => {
28491
+ // Abort mission
28492
+ view.popDialog();
28493
+ ev.preventDefault();
28494
+ }
28495
+ }, glyph("remove"))
28496
+ ]));
28278
28497
  }
28279
- return {
28280
- oninit: waitCheck,
28281
- view: () => {
28282
- return m(".modal-dialog", { id: "accept-dialog", style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: "accept-form" }, [
28283
- m(".chall-hdr", m("table", m("tbody", m("tr", [
28284
- m("td", m("h1.chall-icon", glyph("time"))),
28285
- m("td.l-border", m("h1", oppNick))
28286
- ])))),
28287
- m("div", { "style": { "text-align": "center", "padding-top": "32px" } }, [
28288
- m("p", mt("strong", "Viðureign með klukku")),
28289
- mt("p", oppReady ? "Athuga hvort andstæðingur er reiðubúinn..."
28290
- : ["Andstæðingurinn ", m("strong", oppNick), " er ekki reiðubúinn"])
28291
- ]),
28292
- m(DialogButton, {
28293
- id: 'accept-cancel',
28294
- title: ts('Reyna síðar'),
28295
- onclick: (ev) => {
28296
- // Abort mission
28297
- view.popDialog();
28298
- ev.preventDefault();
28299
- }
28300
- }, glyph("remove"))
28301
- ]));
28302
- }
28303
- };
28304
28498
  };
28305
28499
 
28306
28500
  /*
@@ -31081,58 +31275,38 @@ class Model {
31081
31275
  }
31082
31276
  return false;
31083
31277
  }
31084
- handleUserMessage(json, firstAttach) {
31085
- // Handle an incoming Firebase user message, i.e. a message
31086
- // on the /user/[userid] path
31278
+ handleUserChallengeMessage(json, firstAttach) {
31279
+ // Handle an incoming Firebase user challenge message,
31280
+ // i.e. a message on the /user/[userid]/challenge path
31087
31281
  if (firstAttach || !this.state || !json)
31088
31282
  return;
31089
- let redraw = false;
31090
- if (typeof json.plan === "string") {
31091
- // Potential change of user subscription plan
31092
- if (this.state.plan !== json.plan) {
31093
- this.state.plan = json.plan;
31094
- redraw = true;
31095
- }
31096
- }
31097
- if (json.hasPaid !== undefined) {
31098
- // Potential change of payment status
31099
- const newHasPaid = (this.state.plan !== "" && json.hasPaid) ? true : false;
31100
- if (this.state.hasPaid !== newHasPaid) {
31101
- this.state.hasPaid = newHasPaid;
31102
- redraw = true;
31103
- }
31104
- }
31105
- let invalidateGameList = false;
31106
- // The following code is a bit iffy since both json.challenge and json.move
31107
- // are included in the same message on the /user/[userid] path.
31108
- // !!! FIXME: Split this into two separate listeners,
31109
- // !!! one for challenges and one for moves
31110
- if (json.challenge) {
31111
- // Reload challenge list
31112
- this.loadChallengeList();
31113
- if (this.userListCriteria)
31114
- // We are showing a user list: reload it
31115
- this.loadUserList(this.userListCriteria);
31116
- // Reload game list
31117
- // !!! FIXME: It is strictly speaking not necessary to reload
31118
- // !!! the game list unless this is an acceptance of a challenge
31119
- // !!! (issuance or rejection don't cause the game list to change)
31120
- invalidateGameList = true;
31121
- }
31122
- else if (json.move) {
31123
- // A move has been made in one of this user's games:
31124
- // invalidate the game list (will be loaded upon next display)
31125
- invalidateGameList = true;
31126
- }
31127
- if (invalidateGameList && !this.loadingGameList) {
31283
+ // Reload challenge list
31284
+ this.loadChallengeList();
31285
+ if (this.userListCriteria)
31286
+ // We are showing a user list: reload it
31287
+ this.loadUserList(this.userListCriteria);
31288
+ // Reload game list
31289
+ // !!! FIXME: It is strictly speaking not necessary to reload
31290
+ // !!! the game list unless this is an acceptance of a challenge
31291
+ // !!! (issuance or rejection don't cause the game list to change)
31292
+ if (!this.loadingGameList) {
31128
31293
  this.gameList = null;
31129
- redraw = true;
31130
31294
  }
31131
- if (redraw)
31295
+ m.redraw();
31296
+ }
31297
+ handleUserMoveMessage(json, firstAttach) {
31298
+ // Handle an incoming Firebase user move message,
31299
+ // i.e. a message on the /user/[userid]/move path
31300
+ if (firstAttach || !this.state || !json)
31301
+ return;
31302
+ // A move has been made in one of this user's games:
31303
+ // invalidate the game list (will be loaded upon next display)
31304
+ if (!this.loadingGameList) {
31305
+ this.gameList = null;
31132
31306
  m.redraw();
31307
+ }
31133
31308
  }
31134
31309
  handleMoveMessage(json, firstAttach) {
31135
- var _a;
31136
31310
  // Handle an incoming Firebase move message
31137
31311
  if (!firstAttach && this.game) {
31138
31312
  this.game.update(json);
@@ -31140,7 +31314,7 @@ class Model {
31140
31314
  // - User has audio enabled
31141
31315
  // - User is a participant in the game
31142
31316
  // - This is not a robot game (robots reply instantly anyway)
31143
- if (((_a = this.user) === null || _a === void 0 ? void 0 : _a.audio) && this.game.player !== null && !this.game.isRobotGame()) {
31317
+ if (this.state.audio && this.game.player !== null && !this.game.isRobotGame()) {
31144
31318
  playAudio(this.state, "your-turn");
31145
31319
  }
31146
31320
  m.redraw();
@@ -32583,117 +32757,128 @@ const Main = () => {
32583
32757
  For further information, see https://github.com/mideind/Netskrafl
32584
32758
 
32585
32759
  */
32586
- const UserInfoDialog = (initialVnode) => {
32760
+ const _updateStats = (attrs, state) => {
32761
+ // Fetch the statistics of the given user
32762
+ if (state.loadingStats)
32763
+ return;
32764
+ state.loadingStats = true;
32765
+ const { model } = attrs.view;
32766
+ model.loadUserStats(attrs.userid, (json) => {
32767
+ if (json && json.result === 0)
32768
+ state.stats = json;
32769
+ else
32770
+ state.stats = {};
32771
+ state.loadingStats = false;
32772
+ // m.redraw();
32773
+ });
32774
+ };
32775
+ const _updateRecentList = (attrs, state) => {
32776
+ var _a, _b;
32777
+ // Fetch the recent game list of the given user
32778
+ if (state.loadingRecentList)
32779
+ return;
32780
+ state.loadingRecentList = true;
32781
+ const { model } = attrs.view;
32782
+ model.loadUserRecentList(attrs.userid, state.versusAll ? null : (_b = (_a = model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "", (json) => {
32783
+ if (json && json.result === 0)
32784
+ state.recentList = json.recentlist;
32785
+ else
32786
+ state.recentList = [];
32787
+ state.loadingRecentList = false;
32788
+ // m.redraw();
32789
+ });
32790
+ };
32791
+ const _setVersus = (attrs, state, vsState) => {
32792
+ if (state.versusAll != vsState) {
32793
+ state.versusAll = vsState;
32794
+ state.loadingRecentList = false;
32795
+ _updateRecentList(attrs, state);
32796
+ }
32797
+ };
32798
+ const UserInfoDialog = {
32587
32799
  // A dialog showing the track record of a given user, including
32588
32800
  // recent games and total statistics
32801
+ /*
32589
32802
  const view = initialVnode.attrs.view;
32590
32803
  const model = view.model;
32591
- let stats = {};
32592
- let recentList = [];
32804
+ let stats: UserStats = {};
32805
+ let recentList: RecentListItem[] = [];
32593
32806
  let versusAll = true; // Show games against all opponents or just the current user?
32594
32807
  let loadingStats = false;
32595
32808
  let loadingRecentList = false;
32596
- function _updateStats(vnode) {
32597
- // Fetch the statistics of the given user
32598
- if (loadingStats)
32599
- return;
32600
- loadingStats = true;
32601
- model.loadUserStats(vnode.attrs.userid, (json) => {
32602
- if (json && json.result === 0)
32603
- stats = json;
32604
- else
32605
- stats = {};
32606
- loadingStats = false;
32607
- // m.redraw();
32608
- });
32609
- }
32610
- function _updateRecentList(vnode) {
32611
- var _a, _b;
32612
- // Fetch the recent game list of the given user
32613
- if (loadingRecentList)
32614
- return;
32615
- loadingRecentList = true;
32616
- model.loadUserRecentList(vnode.attrs.userid, versusAll ? null : (_b = (_a = model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "", (json) => {
32617
- if (json && json.result === 0)
32618
- recentList = json.recentlist;
32619
- else
32620
- recentList = [];
32621
- loadingRecentList = false;
32622
- // m.redraw();
32623
- });
32624
- }
32625
- function _setVersus(vnode, vsState) {
32626
- if (versusAll != vsState) {
32627
- versusAll = vsState;
32628
- loadingRecentList = false;
32629
- _updateRecentList(vnode);
32630
- }
32631
- }
32632
- return {
32633
- oninit: (vnode) => {
32634
- _updateRecentList(vnode);
32635
- _updateStats(vnode);
32636
- },
32637
- view: (vnode) => {
32638
- return m(".modal-dialog", { id: 'usr-info-dialog', style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: 'usr-info-form' }, [
32639
- m(".usr-info-hdr", [
32640
- m("h1.usr-info-icon", [
32641
- stats.friend ?
32642
- glyph("coffee-cup", { title: ts('Áskrifandi') }) :
32643
- glyph("user"), nbsp()
32644
- ]),
32645
- m("h1[id='usr-info-nick']", vnode.attrs.nick),
32646
- m("span.vbar", "|"),
32647
- m("h2[id='usr-info-fullname']", vnode.attrs.fullname),
32648
- m(".usr-info-fav", {
32649
- title: ts('Uppáhald'),
32650
- onclick: (ev) => {
32651
- var _a;
32652
- // Toggle the favorite setting
32653
- ev.preventDefault();
32654
- view.actions.toggleFavorite(vnode.attrs.userid, (_a = stats.favorite) !== null && _a !== void 0 ? _a : false);
32655
- stats.favorite = !stats.favorite;
32656
- }
32657
- }, stats.favorite ? glyph("star") : glyph("star-empty"))
32658
- ]),
32659
- m("p", [
32660
- m("strong", t("Nýjustu viðureignir")),
32661
- nbsp(),
32662
- m("span.versus-cat", [
32663
- m("span", {
32664
- class: versusAll ? "shown" : "",
32665
- onclick: () => { _setVersus(vnode, true); } // Set this.versusAll to true
32666
- }, t(" gegn öllum ")),
32667
- m("span", {
32668
- class: versusAll ? "" : "shown",
32669
- onclick: () => { _setVersus(vnode, false); } // Set this.versusAll to false
32670
- }, t(" gegn þér "))
32671
- ])
32809
+ */
32810
+ oninit: (vnode) => {
32811
+ const { attrs, state } = vnode;
32812
+ state.stats = {};
32813
+ state.recentList = [];
32814
+ state.versusAll = true;
32815
+ state.loadingRecentList = false;
32816
+ state.loadingStats = false;
32817
+ _updateRecentList(attrs, state);
32818
+ _updateStats(attrs, state);
32819
+ },
32820
+ view: (vnode) => {
32821
+ const { attrs, state } = vnode;
32822
+ const { view } = attrs;
32823
+ const { stats, recentList, versusAll } = state;
32824
+ return m(".modal-dialog", { id: 'usr-info-dialog', style: { visibility: "visible" } }, m(".ui-widget.ui-widget-content.ui-corner-all", { id: 'usr-info-form' }, [
32825
+ m(".usr-info-hdr", [
32826
+ m("h1.usr-info-icon", [
32827
+ vnode.state.stats.friend ?
32828
+ glyph("coffee-cup", { title: ts('Áskrifandi') }) :
32829
+ glyph("user"), nbsp()
32672
32830
  ]),
32673
- m(".listitem.listheader", [
32674
- m("span.list-win", glyphGrayed("bookmark", { title: ts('Sigur') })),
32675
- mt("span.list-ts-short", "Viðureign lauk"),
32676
- mt("span.list-nick", "Andstæðingur"),
32677
- mt("span.list-scorehdr", "Úrslit"),
32678
- m("span.list-elo-hdr", [
32679
- m("span.glyphicon.glyphicon-user.elo-hdr-left", { title: ts('Mennskir andstæðingar') }),
32680
- "Elo",
32681
- m("span.glyphicon.glyphicon-cog.elo-hdr-right", { title: ts('Allir andstæðingar') })
32682
- ]),
32683
- mt("span.list-duration", "Lengd"),
32684
- m("span.list-manual", glyphGrayed("lightbulb", { title: ts('Keppnishamur') }))
32831
+ m("h1[id='usr-info-nick']", vnode.attrs.nick),
32832
+ m("span.vbar", "|"),
32833
+ m("h2[id='usr-info-fullname']", vnode.attrs.fullname),
32834
+ m(".usr-info-fav", {
32835
+ title: ts('Uppáhald'),
32836
+ onclick: (ev) => {
32837
+ var _a;
32838
+ // Toggle the favorite setting
32839
+ ev.preventDefault();
32840
+ view.actions.toggleFavorite(vnode.attrs.userid, (_a = stats.favorite) !== null && _a !== void 0 ? _a : false);
32841
+ stats.favorite = !stats.favorite;
32842
+ }
32843
+ }, stats.favorite ? glyph("star") : glyph("star-empty"))
32844
+ ]),
32845
+ m("p", [
32846
+ m("strong", t("Nýjustu viðureignir")),
32847
+ nbsp(),
32848
+ m("span.versus-cat", [
32849
+ m("span", {
32850
+ class: versusAll ? "shown" : "",
32851
+ onclick: () => { _setVersus(attrs, state, true); } // Set this.versusAll to true
32852
+ }, t(" gegn öllum ")),
32853
+ m("span", {
32854
+ class: versusAll ? "" : "shown",
32855
+ onclick: () => { _setVersus(attrs, state, false); } // Set this.versusAll to false
32856
+ }, t(" gegn þér "))
32857
+ ])
32858
+ ]),
32859
+ m(".listitem.listheader", [
32860
+ m("span.list-win", glyphGrayed("bookmark", { title: ts('Sigur') })),
32861
+ mt("span.list-ts-short", "Viðureign lauk"),
32862
+ mt("span.list-nick", "Andstæðingur"),
32863
+ mt("span.list-scorehdr", "Úrslit"),
32864
+ m("span.list-elo-hdr", [
32865
+ m("span.glyphicon.glyphicon-user.elo-hdr-left", { title: ts('Mennskir andstæðingar') }),
32866
+ "Elo",
32867
+ m("span.glyphicon.glyphicon-cog.elo-hdr-right", { title: ts('Allir andstæðingar') })
32685
32868
  ]),
32686
- m(RecentList, { view, id: 'usr-recent', recentList }), // Recent game list
32687
- m(StatsDisplay, { view, id: 'usr-stats', ownStats: stats }),
32688
- m(BestDisplay, { id: 'usr-best', ownStats: stats, myself: false }), // Highest word and game scores
32689
- m(DialogButton, {
32690
- id: 'usr-info-close',
32691
- title: ts('Loka'),
32692
- onclick: (ev) => { view.popDialog(); ev.preventDefault(); }
32693
- }, glyph("ok"))
32694
- ]));
32695
- }
32696
- };
32869
+ mt("span.list-duration", "Lengd"),
32870
+ m("span.list-manual", glyphGrayed("lightbulb", { title: ts('Keppnishamur') }))
32871
+ ]),
32872
+ m(RecentList, { view, id: 'usr-recent', recentList }), // Recent game list
32873
+ m(StatsDisplay, { view, id: 'usr-stats', ownStats: stats }),
32874
+ m(BestDisplay, { id: 'usr-best', ownStats: stats, myself: false }), // Highest word and game scores
32875
+ m(DialogButton, {
32876
+ id: 'usr-info-close',
32877
+ title: ts('Loka'),
32878
+ onclick: (ev) => { view.popDialog(); ev.preventDefault(); }
32879
+ }, glyph("ok"))
32880
+ ]));
32881
+ },
32697
32882
  };
32698
32883
 
32699
32884
  /*
@@ -35235,6 +35420,15 @@ class View {
35235
35420
  this.actions = actions;
35236
35421
  // Initialize media listeners now that we have the view reference
35237
35422
  this.actions.initMediaListener(this);
35423
+ // Set up callback to attach user listener when Firebase is ready
35424
+ // This handles both fresh login and cached authentication cases
35425
+ const state = this.model.state;
35426
+ if (state) {
35427
+ state.onFirebaseReady = () => {
35428
+ // Firebase Database is now initialized, attach user listener
35429
+ this.actions.attachListenerToUser();
35430
+ };
35431
+ }
35238
35432
  // Load user preferences early so audio settings are available
35239
35433
  // Use false to not show spinner on initial load
35240
35434
  this.model.loadUser(false);
@@ -35570,8 +35764,8 @@ View.dialogViews = {
35570
35764
  class Actions {
35571
35765
  constructor(model) {
35572
35766
  this.model = model;
35573
- // Media listeners will be initialized when view is available
35574
- // this.attachListenerToUser();
35767
+ // Media and Firebase listeners will be initialized
35768
+ // when view is available
35575
35769
  }
35576
35770
  onNavigateTo(routeName, params, view) {
35577
35771
  var _a, _b;
@@ -35661,30 +35855,35 @@ class Actions {
35661
35855
  }
35662
35856
  onMoveMessage(json, firstAttach) {
35663
35857
  // Handle a move message from Firebase
35664
- console.log("Move message received: " + JSON.stringify(json));
35858
+ // console.log("Move message received: " + JSON.stringify(json));
35665
35859
  this.model.handleMoveMessage(json, firstAttach);
35666
35860
  }
35667
- onUserMessage(json, firstAttach) {
35668
- // Handle a user message from Firebase
35669
- console.log("User message received: " + JSON.stringify(json));
35670
- this.model.handleUserMessage(json, firstAttach);
35861
+ onUserChallengeMessage(json, firstAttach) {
35862
+ // Handle a user challenge message from Firebase
35863
+ // console.log("User challenge message received: " + JSON.stringify(json));
35864
+ this.model.handleUserChallengeMessage(json, firstAttach);
35865
+ }
35866
+ onUserMoveMessage(json, firstAttach) {
35867
+ // Handle a user move message from Firebase
35868
+ // console.log("User move message received: " + JSON.stringify(json));
35869
+ this.model.handleUserMoveMessage(json, firstAttach);
35671
35870
  }
35672
35871
  onChatMessage(json, firstAttach, view) {
35673
- var _a, _b, _c;
35872
+ var _a;
35674
35873
  // Handle an incoming chat message
35675
- if (firstAttach)
35676
- console.log("First attach of chat: " + JSON.stringify(json));
35874
+ if (firstAttach) ;
35677
35875
  else {
35678
- console.log("Chat message received: " + JSON.stringify(json));
35876
+ // console.log("Chat message received: " + JSON.stringify(json));
35679
35877
  if (this.model.addChatMessage(json.game, json.from_userid, json.msg, json.ts)) {
35680
35878
  // A chat message was successfully added
35681
35879
  view.notifyChatMessage();
35682
35880
  // Play audio notification if:
35683
35881
  // - User has audio enabled
35684
35882
  // - Message is from opponent (not from current user)
35685
- const userId = (_b = (_a = this.model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "";
35686
- if (((_c = this.model.user) === null || _c === void 0 ? void 0 : _c.audio) && json.from_userid !== userId) {
35687
- playAudio(this.model.state, "new-msg");
35883
+ const { state } = this.model;
35884
+ const userId = (_a = state.userId) !== null && _a !== void 0 ? _a : "";
35885
+ if (state.audio && json.from_userid !== userId) {
35886
+ playAudio(state, "new-msg");
35688
35887
  }
35689
35888
  }
35690
35889
  }
@@ -35786,12 +35985,17 @@ class Actions {
35786
35985
  }
35787
35986
  attachListenerToUser() {
35788
35987
  const state = this.model.state;
35789
- if (state && state.userId)
35790
- attachFirebaseListener('user/' + state.userId, (json, firstAttach) => this.onUserMessage(json, firstAttach));
35988
+ // console.log(`attachListenerToUser(): userId=${state?.userId}`);
35989
+ if (!state || !state.userId)
35990
+ return;
35991
+ // Listen to challenge and move notifications separately
35992
+ attachFirebaseListener(`user/${state.userId}/challenge`, (json, firstAttach) => this.onUserChallengeMessage(json, firstAttach));
35993
+ attachFirebaseListener(`user/${state.userId}/move`, (json, firstAttach) => this.onUserMoveMessage(json, firstAttach));
35791
35994
  }
35792
35995
  detachListenerFromUser() {
35793
35996
  // Stop listening to Firebase notifications for the current user
35794
35997
  const state = this.model.state;
35998
+ // console.log(`detachListenerFromUser(): userId=${state?.userId}`);
35795
35999
  if (state && state.userId)
35796
36000
  detachFirebaseListener('user/' + state.userId);
35797
36001
  }
@@ -37383,7 +37587,7 @@ async function main(state, container) {
37383
37587
  const locale = state.locale || "is_IS";
37384
37588
  // Log the date being used (helpful for debugging)
37385
37589
  if (dateParam) {
37386
- console.log(`Loading Gáta Dagsins for date: ${validDate} (from URL parameter)`);
37590
+ // console.log(`Loading Gáta Dagsins for date: ${validDate} (from URL parameter)`);
37387
37591
  }
37388
37592
  // Mount the Gáta Dagsins UI using an anonymous closure component
37389
37593
  m.mount(container, {