@mideind/netskrafl-react 1.6.1 → 1.7.0
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/css/netskrafl.css +2 -2
- package/dist/cjs/index.js +236 -72
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/css/netskrafl.css +2 -2
- package/dist/esm/index.js +236 -72
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1898,7 +1898,7 @@ div.netskrafl-tile.dragging div.letterscore {
|
|
|
1898
1898
|
opacity: 1;
|
|
1899
1899
|
}
|
|
1900
1900
|
|
|
1901
|
-
.netskrafl-container div.right-tab.alert > span.glyphicon {
|
|
1901
|
+
.netskrafl-container div.right-tab.chat-alert > span.glyphicon {
|
|
1902
1902
|
color: var(--cancel-button);
|
|
1903
1903
|
animation: redBlink 1s infinite;
|
|
1904
1904
|
-webkit-animation: redBlink 1s infinite;
|
|
@@ -5325,7 +5325,7 @@ div.highlight1.netskrafl-blanktile {
|
|
|
5325
5325
|
}
|
|
5326
5326
|
|
|
5327
5327
|
.netskrafl-container div#user-unfriend {
|
|
5328
|
-
left:
|
|
5328
|
+
left: 166px;
|
|
5329
5329
|
width: 280px;
|
|
5330
5330
|
/* Override */
|
|
5331
5331
|
border-style: solid;
|
package/dist/cjs/index.js
CHANGED
|
@@ -3,8 +3,63 @@
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var React = require('react');
|
|
5
5
|
|
|
6
|
+
const DEFAULT_STATE = {
|
|
7
|
+
projectId: "netskrafl",
|
|
8
|
+
firebaseApiKey: "",
|
|
9
|
+
databaseUrl: "",
|
|
10
|
+
firebaseSenderId: "",
|
|
11
|
+
firebaseAppId: "",
|
|
12
|
+
measurementId: "",
|
|
13
|
+
account: "",
|
|
14
|
+
userEmail: "",
|
|
15
|
+
userId: "",
|
|
16
|
+
userNick: "",
|
|
17
|
+
userFullname: "",
|
|
18
|
+
locale: "is_IS",
|
|
19
|
+
isExplo: false,
|
|
20
|
+
serverUrl: "",
|
|
21
|
+
movesUrl: "",
|
|
22
|
+
movesAccessKey: "",
|
|
23
|
+
token: "",
|
|
24
|
+
loginMethod: "",
|
|
25
|
+
subscriptionUrl: "",
|
|
26
|
+
newUser: false,
|
|
27
|
+
beginner: true,
|
|
28
|
+
fairPlay: false,
|
|
29
|
+
plan: "", // Not a friend
|
|
30
|
+
hasPaid: false,
|
|
31
|
+
ready: true,
|
|
32
|
+
readyTimed: true,
|
|
33
|
+
uiFullscreen: true,
|
|
34
|
+
uiLandscape: false,
|
|
35
|
+
runningLocal: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
6
38
|
// Key for storing auth settings in sessionStorage
|
|
7
39
|
const AUTH_SETTINGS_KEY = "netskrafl_auth_settings";
|
|
40
|
+
const makeServerUrls = (backendUrl, movesUrl) => {
|
|
41
|
+
// If the last character of the url is a slash, cut it off,
|
|
42
|
+
// since path URLs always start with a slash
|
|
43
|
+
const cleanupUrl = (url) => {
|
|
44
|
+
if (url.length > 0 && url[url.length - 1] === "/") {
|
|
45
|
+
url = url.slice(0, -1);
|
|
46
|
+
}
|
|
47
|
+
return url;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
serverUrl: cleanupUrl(backendUrl),
|
|
51
|
+
movesUrl: cleanupUrl(movesUrl),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const makeGlobalState = (overrides) => {
|
|
55
|
+
const state = {
|
|
56
|
+
...DEFAULT_STATE,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
const stateWithUrls = { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
|
|
60
|
+
// Apply any persisted authentication settings from sessionStorage
|
|
61
|
+
return applyPersistedSettings(stateWithUrls);
|
|
62
|
+
};
|
|
8
63
|
// Save authentication settings to sessionStorage
|
|
9
64
|
const saveAuthSettings = (settings) => {
|
|
10
65
|
if (!settings) {
|
|
@@ -100,61 +155,6 @@ const applyPersistedSettings = (state) => {
|
|
|
100
155
|
};
|
|
101
156
|
};
|
|
102
157
|
|
|
103
|
-
const DEFAULT_STATE = {
|
|
104
|
-
projectId: "netskrafl",
|
|
105
|
-
firebaseApiKey: "",
|
|
106
|
-
databaseUrl: "",
|
|
107
|
-
firebaseSenderId: "",
|
|
108
|
-
firebaseAppId: "",
|
|
109
|
-
measurementId: "",
|
|
110
|
-
account: "",
|
|
111
|
-
userEmail: "",
|
|
112
|
-
userId: "",
|
|
113
|
-
userNick: "",
|
|
114
|
-
userFullname: "",
|
|
115
|
-
locale: "is_IS",
|
|
116
|
-
isExplo: false,
|
|
117
|
-
serverUrl: "",
|
|
118
|
-
movesUrl: "",
|
|
119
|
-
movesAccessKey: "",
|
|
120
|
-
token: "",
|
|
121
|
-
loginMethod: "",
|
|
122
|
-
subscriptionUrl: "",
|
|
123
|
-
newUser: false,
|
|
124
|
-
beginner: true,
|
|
125
|
-
fairPlay: false,
|
|
126
|
-
plan: "", // Not a friend
|
|
127
|
-
hasPaid: false,
|
|
128
|
-
ready: true,
|
|
129
|
-
readyTimed: true,
|
|
130
|
-
uiFullscreen: true,
|
|
131
|
-
uiLandscape: false,
|
|
132
|
-
runningLocal: false,
|
|
133
|
-
};
|
|
134
|
-
const makeServerUrls = (backendUrl, movesUrl) => {
|
|
135
|
-
// If the last character of the url is a slash, cut it off,
|
|
136
|
-
// since path URLs always start with a slash
|
|
137
|
-
const cleanupUrl = (url) => {
|
|
138
|
-
if (url.length > 0 && url[url.length - 1] === "/") {
|
|
139
|
-
url = url.slice(0, -1);
|
|
140
|
-
}
|
|
141
|
-
return url;
|
|
142
|
-
};
|
|
143
|
-
return {
|
|
144
|
-
serverUrl: cleanupUrl(backendUrl),
|
|
145
|
-
movesUrl: cleanupUrl(movesUrl),
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
const makeGlobalState = (overrides) => {
|
|
149
|
-
const state = {
|
|
150
|
-
...DEFAULT_STATE,
|
|
151
|
-
...overrides,
|
|
152
|
-
};
|
|
153
|
-
const stateWithUrls = { ...state, ...makeServerUrls(state.serverUrl, state.movesUrl) };
|
|
154
|
-
// Apply any persisted authentication settings from sessionStorage
|
|
155
|
-
return applyPersistedSettings(stateWithUrls);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
158
|
function getDefaultExportFromCjs (x) {
|
|
159
159
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
160
160
|
}
|
|
@@ -2535,6 +2535,133 @@ const ERROR_MESSAGES = {
|
|
|
2535
2535
|
"server": "Netþjónn gat ekki tekið við leiknum - reyndu aftur"
|
|
2536
2536
|
};
|
|
2537
2537
|
|
|
2538
|
+
/*
|
|
2539
|
+
|
|
2540
|
+
Audio.ts
|
|
2541
|
+
|
|
2542
|
+
Audio management service for Netskrafl/Explo
|
|
2543
|
+
|
|
2544
|
+
Copyright (C) 2025 Miðeind ehf.
|
|
2545
|
+
Author: Vilhjálmur Þorsteinsson
|
|
2546
|
+
|
|
2547
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
2548
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
2549
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
2550
|
+
|
|
2551
|
+
*/
|
|
2552
|
+
/**
|
|
2553
|
+
* AudioManager handles preloading and playback of sound effects.
|
|
2554
|
+
* It creates HTMLAudioElement instances for each sound and manages their lifecycle.
|
|
2555
|
+
*/
|
|
2556
|
+
class AudioManager {
|
|
2557
|
+
constructor(state, soundUrls) {
|
|
2558
|
+
this.sounds = new Map();
|
|
2559
|
+
this.initialized = false;
|
|
2560
|
+
// By default, sound URLs are based on /static on the backend server
|
|
2561
|
+
const DEFAULT_SOUND_BASE = serverUrl(state, "/static");
|
|
2562
|
+
const DEFAULT_SOUND_URLS = {
|
|
2563
|
+
"your-turn": `${DEFAULT_SOUND_BASE}/your-turn.mp3`,
|
|
2564
|
+
"you-win": `${DEFAULT_SOUND_BASE}/you-win.mp3`,
|
|
2565
|
+
"new-msg": `${DEFAULT_SOUND_BASE}/new-msg.mp3`,
|
|
2566
|
+
};
|
|
2567
|
+
// Merge provided URLs with defaults
|
|
2568
|
+
this.soundUrls = {
|
|
2569
|
+
...DEFAULT_SOUND_URLS,
|
|
2570
|
+
...(soundUrls || {}),
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Initialize the audio manager by creating and preloading audio elements.
|
|
2575
|
+
* This should be called once when the application starts.
|
|
2576
|
+
*/
|
|
2577
|
+
initialize() {
|
|
2578
|
+
if (this.initialized) {
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
// Create audio elements for each sound
|
|
2582
|
+
Object.entries(this.soundUrls).forEach(([soundId, url]) => {
|
|
2583
|
+
const audio = new Audio(url);
|
|
2584
|
+
audio.preload = "auto";
|
|
2585
|
+
// Handle load errors gracefully - don't let them crash the app
|
|
2586
|
+
audio.addEventListener("error", () => {
|
|
2587
|
+
console.warn(`Failed to load audio: ${soundId} from ${url}`);
|
|
2588
|
+
});
|
|
2589
|
+
this.sounds.set(soundId, audio);
|
|
2590
|
+
});
|
|
2591
|
+
this.initialized = true;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Play a sound by its ID.
|
|
2595
|
+
* If the sound is not loaded or fails to play, the error is logged but doesn't throw.
|
|
2596
|
+
*
|
|
2597
|
+
* @param soundId The identifier of the sound to play
|
|
2598
|
+
*/
|
|
2599
|
+
play(soundId) {
|
|
2600
|
+
if (!this.initialized) {
|
|
2601
|
+
this.initialize();
|
|
2602
|
+
}
|
|
2603
|
+
const audio = this.sounds.get(soundId);
|
|
2604
|
+
if (!audio) {
|
|
2605
|
+
console.warn(`Audio not found: ${soundId}`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
// Reset to start in case it's already playing
|
|
2609
|
+
audio.currentTime = 0;
|
|
2610
|
+
// Play the audio - catch any errors (e.g., user hasn't interacted with page yet)
|
|
2611
|
+
audio.play().catch((err) => {
|
|
2612
|
+
// This is expected in some cases (e.g., autoplay restrictions)
|
|
2613
|
+
// so we just log it at debug level
|
|
2614
|
+
if (err.name !== "NotAllowedError") {
|
|
2615
|
+
console.warn(`Failed to play audio ${soundId}:`, err);
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* Update the URL for a specific sound.
|
|
2621
|
+
* This will recreate the audio element with the new URL.
|
|
2622
|
+
*
|
|
2623
|
+
* @param soundId The identifier of the sound to update
|
|
2624
|
+
* @param url The new URL for the sound
|
|
2625
|
+
*/
|
|
2626
|
+
updateSoundUrl(soundId, url) {
|
|
2627
|
+
this.soundUrls[soundId] = url;
|
|
2628
|
+
// If already initialized, recreate this audio element
|
|
2629
|
+
if (this.initialized) {
|
|
2630
|
+
const audio = new Audio(url);
|
|
2631
|
+
audio.preload = "auto";
|
|
2632
|
+
audio.addEventListener("error", () => {
|
|
2633
|
+
console.warn(`Failed to load audio: ${soundId} from ${url}`);
|
|
2634
|
+
});
|
|
2635
|
+
this.sounds.set(soundId, audio);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Dispose of all audio elements and clean up resources.
|
|
2640
|
+
*/
|
|
2641
|
+
dispose() {
|
|
2642
|
+
this.sounds.forEach((audio) => {
|
|
2643
|
+
audio.pause();
|
|
2644
|
+
audio.src = "";
|
|
2645
|
+
});
|
|
2646
|
+
this.sounds.clear();
|
|
2647
|
+
this.initialized = false;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
// Global singleton instance
|
|
2651
|
+
let audioManager = null;
|
|
2652
|
+
/**
|
|
2653
|
+
* Get or create the global AudioManager instance.
|
|
2654
|
+
*
|
|
2655
|
+
* @param soundUrls Optional custom sound URLs (only used on first call)
|
|
2656
|
+
* @returns The global AudioManager instance
|
|
2657
|
+
*/
|
|
2658
|
+
function getAudioManager(state, soundUrls) {
|
|
2659
|
+
if (!audioManager) {
|
|
2660
|
+
audioManager = new AudioManager(state, soundUrls);
|
|
2661
|
+
}
|
|
2662
|
+
return audioManager;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2538
2665
|
/*
|
|
2539
2666
|
|
|
2540
2667
|
Util.ts
|
|
@@ -2691,11 +2818,10 @@ function setInput(id, val) {
|
|
|
2691
2818
|
const elem = document.getElementById(id);
|
|
2692
2819
|
elem.value = val;
|
|
2693
2820
|
}
|
|
2694
|
-
function playAudio(
|
|
2695
|
-
// Play an audio file
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
sound.play();
|
|
2821
|
+
function playAudio(state, soundId) {
|
|
2822
|
+
// Play an audio file using the AudioManager
|
|
2823
|
+
const audioManager = getAudioManager(state);
|
|
2824
|
+
audioManager.play(soundId);
|
|
2699
2825
|
}
|
|
2700
2826
|
function arrayEqual(a, b) {
|
|
2701
2827
|
// Return true if arrays a and b are equal
|
|
@@ -27907,11 +28033,12 @@ const TogglerReadyTimed = (initialVnode) => {
|
|
|
27907
28033
|
}
|
|
27908
28034
|
};
|
|
27909
28035
|
};
|
|
27910
|
-
const TogglerAudio = () => {
|
|
28036
|
+
const TogglerAudio = (initialVnode) => {
|
|
27911
28037
|
// Toggle for audio on/off
|
|
28038
|
+
const { model } = initialVnode.attrs.view;
|
|
27912
28039
|
function toggleFunc(state) {
|
|
27913
|
-
if (state)
|
|
27914
|
-
playAudio("your-turn");
|
|
28040
|
+
if (state && model.state !== null)
|
|
28041
|
+
playAudio(model.state, "your-turn");
|
|
27915
28042
|
}
|
|
27916
28043
|
return {
|
|
27917
28044
|
view: ({ attrs: { state, tabindex } }) => m(Toggler, {
|
|
@@ -27926,11 +28053,12 @@ const TogglerAudio = () => {
|
|
|
27926
28053
|
})
|
|
27927
28054
|
};
|
|
27928
28055
|
};
|
|
27929
|
-
const TogglerFanfare = () => {
|
|
28056
|
+
const TogglerFanfare = (initialVnode) => {
|
|
27930
28057
|
// Toggle for fanfare on/off
|
|
28058
|
+
const { model } = initialVnode.attrs.view;
|
|
27931
28059
|
function toggleFunc(state) {
|
|
27932
|
-
if (state)
|
|
27933
|
-
playAudio("you-win");
|
|
28060
|
+
if (state && model.state !== null)
|
|
28061
|
+
playAudio(model.state, "you-win");
|
|
27934
28062
|
}
|
|
27935
28063
|
return {
|
|
27936
28064
|
view: ({ attrs: { state, tabindex } }) => m(Toggler, {
|
|
@@ -29077,8 +29205,7 @@ class Game extends BaseGame {
|
|
|
29077
29205
|
// Ongoing timed game: start the clock
|
|
29078
29206
|
this.startClock();
|
|
29079
29207
|
// Kick off loading of chat messages, if this is not a robot game
|
|
29080
|
-
|
|
29081
|
-
if (isHumanGame)
|
|
29208
|
+
if (!this.isRobotGame())
|
|
29082
29209
|
this.loadMessages();
|
|
29083
29210
|
}
|
|
29084
29211
|
init(srvGame) {
|
|
@@ -29236,6 +29363,8 @@ class Game extends BaseGame {
|
|
|
29236
29363
|
// Update the srvGame state with data from the server,
|
|
29237
29364
|
// either after submitting a move to the server or
|
|
29238
29365
|
// after receiving a move notification via the Firebase listener
|
|
29366
|
+
// Remember if the game was already won before this update
|
|
29367
|
+
const wasWon = this.congratulate;
|
|
29239
29368
|
// Stop highlighting the previous opponent move, if any
|
|
29240
29369
|
for (let sq in this.tiles)
|
|
29241
29370
|
if (this.tiles.hasOwnProperty(sq))
|
|
@@ -29256,6 +29385,10 @@ class Game extends BaseGame {
|
|
|
29256
29385
|
// The call to resetClock() clears any outstanding interval timers
|
|
29257
29386
|
// if the srvGame is now over
|
|
29258
29387
|
this.resetClock();
|
|
29388
|
+
// Notify the move listener if the game just transitioned to won
|
|
29389
|
+
if (!wasWon && this.congratulate && this.moveListener) {
|
|
29390
|
+
this.moveListener.notifyGameWon();
|
|
29391
|
+
}
|
|
29259
29392
|
}
|
|
29260
29393
|
;
|
|
29261
29394
|
async refresh() {
|
|
@@ -29460,6 +29593,10 @@ class Game extends BaseGame {
|
|
|
29460
29593
|
// actual game score minus accrued time penalty, if any, in a timed game
|
|
29461
29594
|
return Math.max(this.scores[player] + (player === 0 ? this.penalty0 : this.penalty1), 0);
|
|
29462
29595
|
}
|
|
29596
|
+
isRobotGame() {
|
|
29597
|
+
// Return true if any player in the game is a robot
|
|
29598
|
+
return this.autoplayer[0] || this.autoplayer[1];
|
|
29599
|
+
}
|
|
29463
29600
|
async loadMessages() {
|
|
29464
29601
|
// Load chat messages for this game
|
|
29465
29602
|
if (this.chatLoading)
|
|
@@ -30995,9 +31132,17 @@ class Model {
|
|
|
30995
31132
|
m.redraw();
|
|
30996
31133
|
}
|
|
30997
31134
|
handleMoveMessage(json, firstAttach) {
|
|
31135
|
+
var _a;
|
|
30998
31136
|
// Handle an incoming Firebase move message
|
|
30999
31137
|
if (!firstAttach && this.game) {
|
|
31000
31138
|
this.game.update(json);
|
|
31139
|
+
// Play "your turn" audio notification if:
|
|
31140
|
+
// - User has audio enabled
|
|
31141
|
+
// - User is a participant in the game
|
|
31142
|
+
// - This is not a robot game (robots reply instantly anyway)
|
|
31143
|
+
if (((_a = this.user) === null || _a === void 0 ? void 0 : _a.audio) && this.game.player !== null && !this.game.isRobotGame()) {
|
|
31144
|
+
playAudio(this.state, "your-turn");
|
|
31145
|
+
}
|
|
31001
31146
|
m.redraw();
|
|
31002
31147
|
}
|
|
31003
31148
|
}
|
|
@@ -31008,6 +31153,14 @@ class Model {
|
|
|
31008
31153
|
this.gameList = null;
|
|
31009
31154
|
}
|
|
31010
31155
|
}
|
|
31156
|
+
notifyGameWon() {
|
|
31157
|
+
var _a;
|
|
31158
|
+
// The user just won a game:
|
|
31159
|
+
// play the "you-win" audio if fanfare is enabled
|
|
31160
|
+
if ((_a = this.user) === null || _a === void 0 ? void 0 : _a.fanfare) {
|
|
31161
|
+
playAudio(this.state, "you-win");
|
|
31162
|
+
}
|
|
31163
|
+
}
|
|
31011
31164
|
moreGamesAllowed() {
|
|
31012
31165
|
// Return true if the user is allowed to have more games ongoing
|
|
31013
31166
|
if (!this.state)
|
|
@@ -32719,7 +32872,7 @@ const GamePromptDialogs = (initialVnode) => {
|
|
|
32719
32872
|
// they can be invoked while the last_chall dialog is being
|
|
32720
32873
|
// displayed. We therefore allow them to cover the last_chall
|
|
32721
32874
|
// dialog. On mobile, both dialogs are displayed simultaneously.
|
|
32722
|
-
if (game.last_chall) {
|
|
32875
|
+
if (game.last_chall && game.localturn) {
|
|
32723
32876
|
r.push(m(".chall-info", [
|
|
32724
32877
|
glyph("info-sign"), nbsp(),
|
|
32725
32878
|
// "Your opponent emptied the rack - you can challenge or pass"
|
|
@@ -34336,7 +34489,7 @@ const Tab = {
|
|
|
34336
34489
|
const game = view.model.game;
|
|
34337
34490
|
return m(".right-tab" + (sel === tabid ? ".selected" : ""), {
|
|
34338
34491
|
id: "tab-" + tabid,
|
|
34339
|
-
className: alert ? "alert" : "",
|
|
34492
|
+
className: alert ? "chat-alert" : "",
|
|
34340
34493
|
title: title,
|
|
34341
34494
|
onclick: (ev) => {
|
|
34342
34495
|
// Select this tab
|
|
@@ -34358,7 +34511,7 @@ const TabGroup = {
|
|
|
34358
34511
|
// A group of clickable tabs for the right-side area content
|
|
34359
34512
|
const { view } = vnode.attrs;
|
|
34360
34513
|
const { game } = view.model;
|
|
34361
|
-
const showChat = game && !
|
|
34514
|
+
const showChat = game && !game.isRobotGame();
|
|
34362
34515
|
const r = [
|
|
34363
34516
|
m(Tab, { view, tabid: "board", title: ts("Borðið"), icon: "grid" }),
|
|
34364
34517
|
m(Tab, { view, tabid: "movelist", title: ts("Leikir"), icon: "show-lines" }),
|
|
@@ -34380,7 +34533,7 @@ const TabGroup = {
|
|
|
34380
34533
|
},
|
|
34381
34534
|
// Show chat icon in red if any chat messages have not been seen
|
|
34382
34535
|
// and the chat tab is not already selected
|
|
34383
|
-
alert: !game.chatSeen && view.selectedTab
|
|
34536
|
+
alert: !game.chatSeen && view.selectedTab !== "chat"
|
|
34384
34537
|
}));
|
|
34385
34538
|
}
|
|
34386
34539
|
return m.fragment({}, r);
|
|
@@ -35082,6 +35235,9 @@ class View {
|
|
|
35082
35235
|
this.actions = actions;
|
|
35083
35236
|
// Initialize media listeners now that we have the view reference
|
|
35084
35237
|
this.actions.initMediaListener(this);
|
|
35238
|
+
// Load user preferences early so audio settings are available
|
|
35239
|
+
// Use false to not show spinner on initial load
|
|
35240
|
+
this.model.loadUser(false);
|
|
35085
35241
|
}
|
|
35086
35242
|
appView(routeName) {
|
|
35087
35243
|
// Returns a view based on the current route.
|
|
@@ -35514,6 +35670,7 @@ class Actions {
|
|
|
35514
35670
|
this.model.handleUserMessage(json, firstAttach);
|
|
35515
35671
|
}
|
|
35516
35672
|
onChatMessage(json, firstAttach, view) {
|
|
35673
|
+
var _a, _b, _c;
|
|
35517
35674
|
// Handle an incoming chat message
|
|
35518
35675
|
if (firstAttach)
|
|
35519
35676
|
console.log("First attach of chat: " + JSON.stringify(json));
|
|
@@ -35522,6 +35679,13 @@ class Actions {
|
|
|
35522
35679
|
if (this.model.addChatMessage(json.game, json.from_userid, json.msg, json.ts)) {
|
|
35523
35680
|
// A chat message was successfully added
|
|
35524
35681
|
view.notifyChatMessage();
|
|
35682
|
+
// Play audio notification if:
|
|
35683
|
+
// - User has audio enabled
|
|
35684
|
+
// - Message is from opponent (not from current user)
|
|
35685
|
+
const userId = (_b = (_a = this.model.state) === null || _a === void 0 ? void 0 : _a.userId) !== null && _b !== void 0 ? _b : "";
|
|
35686
|
+
if (((_c = this.model.user) === null || _c === void 0 ? void 0 : _c.audio) && json.from_userid !== userId) {
|
|
35687
|
+
playAudio(this.model.state, "new-msg");
|
|
35688
|
+
}
|
|
35525
35689
|
}
|
|
35526
35690
|
}
|
|
35527
35691
|
}
|