@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/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
27532
|
-
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
27542
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
27570
|
-
// user info may have changed
|
|
27738
|
+
// Redraw UI to reflect authentication changes
|
|
27571
27739
|
m.redraw();
|
|
27572
|
-
//
|
|
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
|
|
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 =
|
|
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
|
-
|
|
27832
|
-
|
|
27833
|
-
|
|
27834
|
-
|
|
27835
|
-
|
|
27836
|
-
|
|
27837
|
-
|
|
27838
|
-
|
|
27839
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
|
|
27845
|
-
|
|
27846
|
-
|
|
27847
|
-
|
|
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 =
|
|
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
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
|
|
28137
|
-
|
|
28138
|
-
|
|
28139
|
-
|
|
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
|
-
|
|
28179
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
28194
|
-
|
|
28195
|
-
|
|
28196
|
-
|
|
28197
|
-
|
|
28198
|
-
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
28206
|
-
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
|
|
28210
|
-
|
|
28211
|
-
|
|
28212
|
-
|
|
28213
|
-
|
|
28214
|
-
|
|
28215
|
-
|
|
28216
|
-
|
|
28217
|
-
|
|
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(
|
|
28220
|
-
|
|
28221
|
-
|
|
28222
|
-
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
28226
|
-
|
|
28227
|
-
|
|
28228
|
-
|
|
28229
|
-
|
|
28230
|
-
|
|
28231
|
-
|
|
28410
|
+
m("p", [
|
|
28411
|
+
m("strong", ["2 x ", duration.toString(), ts(" mínútur.")])
|
|
28412
|
+
]),
|
|
28413
|
+
mt("p", [
|
|
28414
|
+
"Beðið er eftir að á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
|
-
|
|
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 =
|
|
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
|
-
|
|
28244
|
-
|
|
28245
|
-
|
|
28246
|
-
|
|
28247
|
-
|
|
28248
|
-
|
|
28249
|
-
|
|
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(
|
|
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
|
-
|
|
28276
|
-
|
|
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
|
-
|
|
31085
|
-
// Handle an incoming Firebase user 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
|
-
|
|
31090
|
-
|
|
31091
|
-
|
|
31092
|
-
|
|
31093
|
-
|
|
31094
|
-
|
|
31095
|
-
|
|
31096
|
-
|
|
31097
|
-
|
|
31098
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
32597
|
-
|
|
32598
|
-
|
|
32599
|
-
|
|
32600
|
-
|
|
32601
|
-
|
|
32602
|
-
|
|
32603
|
-
|
|
32604
|
-
|
|
32605
|
-
|
|
32606
|
-
|
|
32607
|
-
|
|
32608
|
-
}
|
|
32609
|
-
|
|
32610
|
-
|
|
32611
|
-
|
|
32612
|
-
|
|
32613
|
-
|
|
32614
|
-
|
|
32615
|
-
|
|
32616
|
-
|
|
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(".
|
|
32674
|
-
|
|
32675
|
-
|
|
32676
|
-
|
|
32677
|
-
|
|
32678
|
-
|
|
32679
|
-
|
|
32680
|
-
|
|
32681
|
-
|
|
32682
|
-
|
|
32683
|
-
|
|
32684
|
-
|
|
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
|
-
|
|
32687
|
-
m(
|
|
32688
|
-
|
|
32689
|
-
|
|
32690
|
-
|
|
32691
|
-
|
|
32692
|
-
|
|
32693
|
-
|
|
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
|
|
35574
|
-
//
|
|
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
|
-
|
|
35668
|
-
// Handle a user message from Firebase
|
|
35669
|
-
console.log("User message received: " + JSON.stringify(json));
|
|
35670
|
-
this.model.
|
|
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
|
|
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
|
|
35686
|
-
|
|
35687
|
-
|
|
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
|
-
|
|
35790
|
-
|
|
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, {
|