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