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