@mideind/netskrafl-react 1.0.0-beta.6 → 1.0.0-beta.8

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.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/css/netskrafl.css +1508 -215
  3. package/dist/cjs/index.js +9018 -7764
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/esm/css/netskrafl.css +1508 -215
  6. package/dist/esm/index.js +9017 -7764
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/types.d.ts +4 -1
  9. package/package.json +13 -2
  10. package/.eslintignore +0 -8
  11. package/.eslintrc.json +0 -13
  12. package/rollup.config.js +0 -67
  13. package/src/components/index.ts +0 -2
  14. package/src/components/netskrafl/Netskrafl.stories.tsx +0 -66
  15. package/src/components/netskrafl/Netskrafl.tsx +0 -155
  16. package/src/components/netskrafl/Netskrafl.types.ts +0 -7
  17. package/src/components/netskrafl/index.ts +0 -2
  18. package/src/css/fonts.css +0 -4
  19. package/src/css/glyphs.css +0 -223
  20. package/src/css/main.css +0 -4
  21. package/src/css/skrafl-explo.css +0 -6636
  22. package/src/fonts/glyphicons-regular.eot +0 -0
  23. package/src/fonts/glyphicons-regular.ttf +0 -0
  24. package/src/fonts/glyphicons-regular.woff +0 -0
  25. package/src/index.ts +0 -4
  26. package/src/messages/messages.json +0 -1576
  27. package/src/mithril/actions.ts +0 -319
  28. package/src/mithril/bag.ts +0 -65
  29. package/src/mithril/bestdisplay.ts +0 -74
  30. package/src/mithril/blankdialog.ts +0 -94
  31. package/src/mithril/board.ts +0 -339
  32. package/src/mithril/buttons.ts +0 -303
  33. package/src/mithril/challengedialog.ts +0 -186
  34. package/src/mithril/channel.ts +0 -162
  35. package/src/mithril/chat.ts +0 -228
  36. package/src/mithril/components.ts +0 -496
  37. package/src/mithril/dragdrop.ts +0 -219
  38. package/src/mithril/elopage.ts +0 -202
  39. package/src/mithril/friend.ts +0 -227
  40. package/src/mithril/game.ts +0 -1378
  41. package/src/mithril/gameview.ts +0 -111
  42. package/src/mithril/globalstate.ts +0 -33
  43. package/src/mithril/i18n.ts +0 -187
  44. package/src/mithril/localstorage.ts +0 -133
  45. package/src/mithril/login.ts +0 -122
  46. package/src/mithril/logo.ts +0 -323
  47. package/src/mithril/main.ts +0 -755
  48. package/src/mithril/mithril.ts +0 -29
  49. package/src/mithril/model.ts +0 -855
  50. package/src/mithril/movelistitem.ts +0 -226
  51. package/src/mithril/page.ts +0 -856
  52. package/src/mithril/playername.ts +0 -91
  53. package/src/mithril/promodialog.ts +0 -82
  54. package/src/mithril/recentlist.ts +0 -148
  55. package/src/mithril/request.ts +0 -52
  56. package/src/mithril/review.ts +0 -634
  57. package/src/mithril/rightcolumn.ts +0 -398
  58. package/src/mithril/searchbutton.ts +0 -118
  59. package/src/mithril/statsdisplay.ts +0 -109
  60. package/src/mithril/tabs.ts +0 -169
  61. package/src/mithril/tile.ts +0 -145
  62. package/src/mithril/twoletter.ts +0 -76
  63. package/src/mithril/types.ts +0 -384
  64. package/src/mithril/userinfodialog.ts +0 -171
  65. package/src/mithril/util.ts +0 -304
  66. package/src/mithril/wait.ts +0 -246
  67. package/src/mithril/wordcheck.ts +0 -102
  68. package/tsconfig.json +0 -28
  69. package/vite.config.ts +0 -12
@@ -1,855 +0,0 @@
1
- /*
2
-
3
- Model.ts
4
-
5
- Single page UI for Explo/Netskrafl using the Mithril library
6
-
7
- Copyright (C) 2024 Miðeind ehf.
8
- Author: Vilhjálmur Þorsteinsson
9
-
10
- The Creative Commons Attribution-NonCommercial 4.0
11
- International Public License (CC-BY-NC 4.0) applies to this software.
12
- For further information, see https://github.com/mideind/Netskrafl
13
-
14
- This file implements the Model class and related global state.
15
- A Model encapsulates the data, including the Game instance,
16
- that is being displayed live by the current view.
17
-
18
- */
19
-
20
- import { GlobalState } from "./globalstate";
21
- import {
22
- ServerGame, Move, IModel, UserListItem,
23
- ChallengeParameters, GameListItem, ChallengeListItem, RecentListItem,
24
- UserPrefs, MovesRequest, BestMoves,
25
- UserErrors,
26
- UserListCriteria,
27
- UserStats,
28
- EloListSelection,
29
- } from "./types";
30
- import { m } from "./mithril";
31
- import { logEvent } from "./channel";
32
- import { loadMessages } from "./i18n";
33
- import { request, requestMoves } from "./request";
34
- import { Game, MAX_OVERTIME, DEBUG_OVERTIME } from "./game";
35
- import { Params } from "./util";
36
-
37
- // Maximum number of concurrent games per user
38
- const MAX_GAMES = 50;
39
- // Maximum number of concurrent games for non-paying users
40
- const MAX_FREE_EXPLO = 3;
41
- const MAX_FREE_NETSKRAFL = 8;
42
- // Number of best moves to show in the review screen
43
- const NUM_BEST_MOVES = 19;
44
-
45
- // Basic Mithril routing settings
46
- interface Path {
47
- name: string;
48
- route: string;
49
- mustLogin: boolean;
50
- }
51
-
52
- type Paths = Path[];
53
-
54
- interface Settings {
55
- paths: Paths;
56
- defaultRoute: string;
57
- }
58
-
59
- export function getSettings(): Settings {
60
- // Returns an app-wide settings object, used by Mithril for routing
61
- const
62
- paths: Paths = [
63
- { name: "main", route: "/main", mustLogin: true },
64
- { name: "help", route: "/help", mustLogin: false },
65
- { name: "thanks", route: "/thanks", mustLogin: true },
66
- { name: "cancel", route: "/cancel", mustLogin: true },
67
- { name: "confirm", route: "/confirm", mustLogin: true },
68
- { name: "game", route: "/game/:uuid", mustLogin: true },
69
- { name: "review", route: "/review/:uuid", mustLogin: true },
70
- { name: "login", route: "/login", mustLogin: false },
71
- { name: "loginerror", route: "/loginerror", mustLogin: false },
72
- ];
73
- return {
74
- paths: paths,
75
- defaultRoute: paths[0].route
76
- };
77
- }
78
-
79
- export class Model implements IModel {
80
-
81
- // A class for the underlying data model, displayed by the current view
82
-
83
- state: GlobalState | null = null;
84
- paths: Paths = [];
85
- // The routeName will be "login", "main", "game"...
86
- routeName?: string = undefined;
87
- // Eventual parameters within the route URL, such as the game uuid
88
- params?: Params = undefined;
89
- // The current game being displayed, if any
90
- game: Game | null = null;
91
- // The current game list
92
- gameList: GameListItem[] | null = null;
93
- // Number of games where it's the player's turn, plus count of zombie games
94
- numGames = 0;
95
- loadingGameList = false;
96
- // The current challenge list
97
- challengeList: ChallengeListItem[] | null = null;
98
- // Sum up received challenges and issued timed challenges where the opponent is ready
99
- numChallenges = 0;
100
- loadingChallengeList = false;
101
- // Number of opponents who are ready and waiting for a timed game
102
- oppReady = 0;
103
- // Recent games
104
- recentList: RecentListItem[] | null = null;
105
- loadingRecentList = false;
106
- // The currently displayed user list
107
- userListCriteria: { query: string; spec: string; } | null | undefined = null;
108
- userList: UserListItem[] | null | undefined = null;
109
- loadingUserList = false;
110
- // The currently displayed Elo rating list
111
- eloRatingList: UserListItem[] | null | undefined = null;
112
- eloRatingSpec: EloListSelection | null | undefined = null;
113
- // The user's own statistics
114
- ownStats: Record<string, any> | null = null;
115
- // The current user information being edited, if any
116
- user: UserPrefs | null | undefined = null;
117
- userErrors: UserErrors | null = null;
118
- userLoadError: boolean = false;
119
- // The (cached) help screen contents
120
- helpHTML: string | null = null;
121
- // The (cached) friend promo screen contents
122
- friendHTML: string | null = null;
123
- // Outstanding server requests
124
- spinners: number = 0;
125
- // The index of the game move being reviewed, if any
126
- reviewMove: number | null = null;
127
- // The best moves available at this stage, if reviewing game
128
- bestMoves: Move[] | null = null;
129
- // The index of the best move being highlighted, if reviewing game
130
- highlightedMove: number | null = null;
131
- // Maximum number of free games allowed concurrently
132
- maxFreeGames = 0;
133
- isExplo = false;
134
-
135
- constructor(settings: Settings, state: GlobalState) {
136
- this.paths = settings.paths.slice();
137
- this.state = state;
138
- this.isExplo = state.isExplo;
139
- this.maxFreeGames = state.isExplo ? MAX_FREE_EXPLO : MAX_FREE_NETSKRAFL;
140
- // Load localized text messages from the messages.json file
141
- loadMessages(state.locale);
142
- }
143
-
144
- async loadGame(uuid: string, funcComplete: () => void, deleteZombie: boolean = false) {
145
- // Fetch a game state from the server, given the game's UUID.
146
- // If deleteZombie is true, we are loading a zombie game for
147
- // inspection, so we tell the server to remove the zombie marker.
148
- try {
149
- if (this.game !== null)
150
- // We have a prior game in memory:
151
- // clean it up before allocating the new one
152
- this.game.cleanup();
153
- this.game = null;
154
- this.reviewMove = null;
155
- this.bestMoves = null;
156
- this.highlightedMove = null;
157
- if (!uuid) return; // Should not happen
158
- const result: { ok: boolean; game: ServerGame; } = await request({
159
- method: "POST",
160
- url: "/gamestate",
161
- body: {
162
- game: uuid,
163
- delete_zombie: deleteZombie
164
- }
165
- });
166
- if (!result?.ok) {
167
- // console.log("Game " + uuid + " could not be loaded");
168
- }
169
- else {
170
- // Create a new game instance and load the state into it
171
- this.game = new Game(uuid, result.game, this, this.state?.runningLocal ? DEBUG_OVERTIME : MAX_OVERTIME);
172
- // Successfully loaded: call the completion function, if given
173
- // (this usually attaches the Firebase event listener)
174
- if (funcComplete !== undefined)
175
- funcComplete();
176
- if (!this.state?.uiFullscreen)
177
- // Mobile UI: show board tab
178
- this.game.setSelectedTab("board");
179
- }
180
- } catch(e) {
181
- // If new game cannot be loaded, keep the old one in place
182
- }
183
- }
184
-
185
- async loadGameList(includeZombies: boolean = true) {
186
- // Load the list of currently active games for this user
187
- if (this.loadingGameList)
188
- // Already loading
189
- return;
190
- this.loadingGameList = true; // Loading in progress
191
- this.gameList = [];
192
- this.numGames = 0;
193
- this.spinners++;
194
- try {
195
- const json: { result: number; gamelist: GameListItem[]; } = await request({
196
- method: "POST",
197
- url: "/gamelist",
198
- body: { zombie: includeZombies }
199
- });
200
- if (!json || json.result !== 0) {
201
- // An error occurred
202
- this.gameList = [];
203
- return;
204
- }
205
- this.gameList = json.gamelist || [];
206
- if (this.gameList)
207
- // Sum up games where it's the player's turn, as well as zombie games
208
- this.numGames = this.gameList.reduce(
209
- (acc, item) => acc + (item.my_turn || item.zombie ? 1 : 0), 0
210
- );
211
- } catch(e) {
212
- this.gameList = [];
213
- } finally {
214
- this.loadingGameList = false;
215
- if (this.spinners)
216
- this.spinners--;
217
- }
218
- }
219
-
220
- async loadChallengeList() {
221
- // Load the list of current challenges (received and issued)
222
- if (this.loadingChallengeList)
223
- return;
224
- this.loadingChallengeList = true;
225
- this.challengeList = [];
226
- this.numChallenges = 0;
227
- this.oppReady = 0;
228
- try {
229
- const json: { result: number; challengelist: ChallengeListItem[]; } = await request({
230
- method: "POST",
231
- url: "/challengelist"
232
- });
233
- if (!json || json.result !== 0) {
234
- // An error occurred
235
- this.challengeList = [];
236
- return;
237
- }
238
- this.challengeList = json.challengelist || [];
239
- // Count opponents who are ready and waiting for timed games
240
- for (let ch of this.challengeList) {
241
- if (ch.opp_ready)
242
- this.oppReady++;
243
- }
244
- this.numChallenges = this.oppReady;
245
- if (this.challengeList)
246
- // Sum up received challenges and issued timed challenges where
247
- // the opponent is ready
248
- this.numChallenges += this.challengeList.reduce(
249
- (acc, item) => acc + (item.received ? 1 : 0), 0
250
- );
251
- } catch(e) {
252
- this.challengeList = [];
253
- } finally {
254
- this.loadingChallengeList = false;
255
- }
256
- }
257
-
258
- async loadRecentList() {
259
- // Load the list of recent games for this user
260
- if (this.loadingRecentList)
261
- return;
262
- this.loadingRecentList = true; // Prevent concurrent loading
263
- this.recentList = [];
264
- try {
265
- const json: { result: number; recentlist: RecentListItem[]; } = await request({
266
- method: "POST",
267
- url: "/recentlist",
268
- body: { versus: null, count: 40 }
269
- });
270
- if (!json || json.result !== 0) {
271
- // An error occurred
272
- this.recentList = [];
273
- return;
274
- }
275
- this.recentList = json.recentlist || [];
276
- } catch(e) {
277
- this.recentList = [];
278
- } finally {
279
- this.loadingRecentList = false;
280
- }
281
- }
282
-
283
- async loadUserRecentList(userid: string, versus: string | null, readyFunc: (json: any) => void) {
284
- // Load the list of recent games for the given user
285
- const json: any = await request({
286
- method: "POST",
287
- url: "/recentlist",
288
- body: { user: userid, versus: versus, count: 40 }
289
- });
290
- readyFunc(json);
291
- }
292
-
293
- async loadUserList(
294
- criteria: UserListCriteria,
295
- activateSpinner: boolean
296
- ) {
297
- // Load a list of users according to the given criteria
298
- if (criteria.query === "search" && criteria.spec === "") {
299
- // Optimize by not sending an empty search query to the server,
300
- // since it always returns an empty list
301
- this.userList = [];
302
- this.userListCriteria = criteria;
303
- m.redraw(); // Call this explicitly as we're not calling request()
304
- return;
305
- }
306
- this.userList = undefined;
307
- this.userListCriteria = undefined; // Marker to prevent concurrent loading
308
- if (activateSpinner) {
309
- // This will show a spinner overlay, disabling clicks on
310
- // all underlying controls
311
- this.spinners++;
312
- }
313
- const url = "/userlist";
314
- const body: { query?: string; spec?: string; kind?: string; } = criteria;
315
- try {
316
- const json = await request<{
317
- result: number;
318
- userlist: UserListItem[];
319
- }>({
320
- method: "POST",
321
- url,
322
- body,
323
- });
324
- if (!json || json.result !== 0) {
325
- // An error occurred
326
- this.userList = [];
327
- this.userListCriteria = criteria;
328
- return;
329
- }
330
- this.userList = json.userlist;
331
- this.userListCriteria = criteria;
332
- } catch(e) {
333
- this.userList = [];
334
- this.userListCriteria = criteria;
335
- } finally {
336
- if (activateSpinner && this.spinners)
337
- // Remove spinner overlay, if present
338
- this.spinners--;
339
- }
340
- }
341
-
342
- async loadEloRatingList(
343
- spec: EloListSelection,
344
- activateSpinner: boolean,
345
- ) {
346
- // Load an Elo rating list according to the given criteria
347
- this.eloRatingList = undefined;
348
- this.eloRatingSpec = undefined; // Marker to prevent concurrent loading
349
- if (activateSpinner) {
350
- // This will show a spinner overlay, disabling clicks on
351
- // all underlying controls
352
- this.spinners++;
353
- }
354
- const url = "/rating";
355
- const body = { kind: spec };
356
- try {
357
- const json = await request<{
358
- result: number;
359
- rating: UserListItem[];
360
- }>({
361
- method: "POST",
362
- url,
363
- body,
364
- });
365
- if (!json || json.result !== 0) {
366
- // An error occurred
367
- this.eloRatingList = [];
368
- this.eloRatingSpec = spec;
369
- return;
370
- }
371
- this.eloRatingList = json.rating;
372
- this.eloRatingSpec = spec;
373
- } catch(e) {
374
- this.eloRatingList = [];
375
- this.eloRatingSpec = spec;
376
- } finally {
377
- if (activateSpinner && this.spinners)
378
- // Remove spinner overlay, if present
379
- this.spinners--;
380
- }
381
- }
382
-
383
- async loadOwnStats() {
384
- // Load statistics for the current user
385
- this.ownStats = {};
386
- try {
387
- const json: { result: number; } = await request({
388
- method: "POST",
389
- url: "/userstats",
390
- body: {} // Current user is implicit
391
- });
392
- if (!json || json.result !== 0) {
393
- // An error occurred
394
- return;
395
- }
396
- this.ownStats = json;
397
- } catch(e) {
398
- }
399
- }
400
-
401
- async loadUserStats(userid: string, readyFunc: (json: UserStats) => void) {
402
- // Load statistics for the given user
403
- try {
404
- const json = await request<UserStats>({
405
- method: "POST",
406
- url: "/userstats",
407
- body: { user: userid }
408
- });
409
- readyFunc(json);
410
- } catch(e) {
411
- // No need to do anything
412
- }
413
- }
414
-
415
- async loadPromoContent(key: string, readyFunc: (html: string) => void) {
416
- // Load HTML content for promo dialog
417
- try {
418
- const html: string = await request({
419
- method: "POST",
420
- url: "/promo",
421
- body: { key: key },
422
- responseType: "text",
423
- deserialize: (str: string) => str
424
- });
425
- readyFunc(html);
426
- } catch(e) {
427
- // No need to do anything
428
- }
429
- }
430
-
431
- async loadBestMoves(moveIndex: number) {
432
- // Load the best moves available at a given state in a game
433
- if (!this.game || !this.game.uuid)
434
- return;
435
- if (!moveIndex) {
436
- // No moves to load, but display summary
437
- this.reviewMove = 0;
438
- this.bestMoves = null;
439
- this.highlightedMove = null;
440
- this.game.setRack([]);
441
- this.game.placeTiles(0);
442
- return;
443
- }
444
- const tsc = this.game.tile_scores;
445
- const rack = this.game.rackAtMove(moveIndex - 1);
446
- if (!rack || this.game.isFinalMove(moveIndex - 1)) {
447
- // If there is no rack, or if this is a final (adjustment) move,
448
- // there are no best moves
449
- this.reviewMove = moveIndex;
450
- this.bestMoves = [];
451
- this.highlightedMove = null;
452
- if (!rack) {
453
- this.game.setRack([]);
454
- } else {
455
- this.game.setRack(rack.split("").map(tile =>
456
- [tile, tsc[tile]]
457
- ));
458
- }
459
- this.game.placeTiles(moveIndex);
460
- return;
461
- }
462
- // Don't display navigation buttons while fetching best moves
463
- this.reviewMove = null;
464
- // ...but do display a spinner, if this takes too long
465
- this.spinners++;
466
- try {
467
- const rq: MovesRequest = {
468
- locale: this.game.locale,
469
- board_type: this.isExplo ? "explo" : "standard",
470
- board: this.game.boardAsStrings(),
471
- rack,
472
- limit: NUM_BEST_MOVES,
473
- };
474
- const json = await requestMoves<BestMoves>({
475
- method: "POST",
476
- url: "/moves",
477
- body: rq,
478
- });
479
- this.highlightedMove = null;
480
- if (!json || json.moves === undefined) {
481
- // Something unexpected going on
482
- this.reviewMove = null;
483
- this.bestMoves = null;
484
- return;
485
- }
486
- this.reviewMove = moveIndex;
487
- this.bestMoves = json.moves.map((m) => {
488
- return [((moveIndex - 1) % 2) as 0 | 1, [m.co, m.w, m.sc], false];
489
- });
490
- this.game.setRack(rack.split("").map(tile =>
491
- [tile, tsc[tile]]
492
- ));
493
- // Populate the board cells with only the tiles
494
- // laid down up and until the indicated moveIndex
495
- this.game.placeTiles(moveIndex);
496
- } catch(e) {
497
- this.highlightedMove = null;
498
- this.reviewMove = null;
499
- this.bestMoves = null;
500
- } finally {
501
- if (this.spinners)
502
- this.spinners--;
503
- }
504
- }
505
-
506
- async loadHelp() {
507
- // Load the help screen HTML from the server
508
- // (this is done the first time the help is displayed)
509
- if (this.helpHTML !== null)
510
- return; // Already loaded
511
- try {
512
- const locale = this.state?.locale || "is_IS";
513
- const result: string = await request({
514
- method: "GET",
515
- url: "/rawhelp?version=malstadur&locale=" + locale,
516
- responseType: "text",
517
- deserialize: (str: string) => str
518
- });
519
- this.helpHTML = result;
520
- } catch(e) {
521
- this.helpHTML = "";
522
- }
523
- }
524
-
525
- async loadFriendPromo() {
526
- // Load the friend promo HTML from the server
527
- // (this is done the first time the dialog is displayed)
528
- if (this.friendHTML !== null)
529
- return; // Already loaded
530
- try {
531
- const locale = this.state?.locale || "is_IS";
532
- const result: string = await request({
533
- method: "GET",
534
- url: "/friend?locale=" + locale,
535
- responseType: "text",
536
- deserialize: (str: string) => str
537
- });
538
- this.friendHTML = result;
539
- } catch(e) {
540
- this.friendHTML = "";
541
- }
542
- }
543
-
544
- async loadUser(activateSpinner: boolean) {
545
- // Fetch the preferences of the currently logged in user, if any
546
- this.user = undefined;
547
- if (activateSpinner) {
548
- // This will show a spinner overlay, disabling clicks on
549
- // all underlying controls
550
- this.spinners++;
551
- }
552
- try {
553
- const result: { ok: boolean; userprefs: UserPrefs; } = await request({
554
- method: "POST",
555
- url: "/loaduserprefs",
556
- });
557
- if (!result || !result.ok) {
558
- this.user = null;
559
- this.userErrors = null;
560
- this.userLoadError = true;
561
- }
562
- else {
563
- this.user = result.userprefs;
564
- this.userErrors = null;
565
- }
566
- } catch(e) {
567
- this.user = null;
568
- this.userErrors = null;
569
- this.userLoadError = true;
570
- } finally {
571
- if (activateSpinner && this.spinners)
572
- this.spinners--;
573
- }
574
- }
575
-
576
- async saveUser(successFunc: () => void) {
577
- // Update the preferences of the currently logged in user, if any
578
- const user = this.user;
579
- if (!user) return;
580
- try {
581
- const result: { ok: boolean; err?: UserErrors; } = await request({
582
- method: "POST",
583
- url: "/saveuserprefs",
584
- body: user
585
- });
586
- if (result?.ok) {
587
- // User preferences modified successfully on the server:
588
- // update the state variables that we're caching
589
- const state = this.state;
590
- if (state !== null) {
591
- state.userNick = user.nickname;
592
- state.beginner = user.beginner;
593
- state.fairPlay = user.fairplay;
594
- }
595
- // Note that state.plan is updated via a Firebase notification
596
- // Give the game instance a chance to update its state
597
- if (this.game !== null)
598
- this.game.notifyUserChange(user.nickname);
599
- // Complete: call success function
600
- if (successFunc !== undefined)
601
- successFunc();
602
- // Reset errors
603
- this.userErrors = null;
604
- // Ensure that a fresh instance is loaded next time
605
- this.user = null;
606
- }
607
- else {
608
- // Error saving user prefs: show details, if available
609
- this.userErrors = result.err || null;
610
- }
611
- } catch(e) {
612
- this.userErrors = null;
613
- }
614
- }
615
-
616
- async setUserPref(pref: Record<string, any>) {
617
- // Set a user preference
618
- try {
619
- await request(
620
- {
621
- method: "POST",
622
- url: "/setuserpref",
623
- body: pref
624
- }
625
- ); // No result required or expected
626
- } catch (e) {
627
- // A future TODO might be to signal an error in the UI
628
- }
629
- }
630
-
631
- async newGame(oppid: string, reverse: boolean) {
632
- // Ask the server to initiate a new game against the given opponent
633
- try {
634
- const rqBody: {
635
- opp: string;
636
- rev: boolean;
637
- board_type?: string
638
- } = { opp: oppid, rev: reverse };
639
- if (this.isExplo) {
640
- // On an Explo client, always use the Explo board,
641
- // regardless of the user's locale setting
642
- rqBody.board_type = "explo";
643
- }
644
- const rq = {
645
- method: "POST",
646
- url: "/initgame",
647
- body: rqBody
648
- };
649
- const json = await request<{ ok: boolean; uuid: string; }>(rq);
650
- if (json?.ok) {
651
- // Log the new game event
652
- const locale = this.state?.locale || "is_IS";
653
- logEvent("new_game",
654
- {
655
- uuid: json.uuid,
656
- timed: reverse,
657
- locale
658
- }
659
- );
660
- // Go to the newly created game
661
- m.route.set("/game/" + json.uuid);
662
- }
663
- } catch(e) {
664
- // No need to do anything
665
- }
666
- }
667
-
668
- async modifyChallenge(parameters: ChallengeParameters) {
669
- // Reject or retract a challenge
670
- try {
671
- const json: { result: number; } = await request({
672
- method: "POST",
673
- url: "/challenge",
674
- body: parameters
675
- });
676
- if (json?.result === 0) {
677
- // Log the change of challenge status (issue/decline/retract/accept)
678
- const locale = this.state?.locale || "is_IS";
679
- var p: any = { locale };
680
- if (parameters.duration !== undefined)
681
- p.duration = parameters.duration;
682
- if (parameters.fairplay !== undefined)
683
- p.fairplay = parameters.fairplay;
684
- if (parameters.manual !== undefined)
685
- p.manual = parameters.manual;
686
- logEvent("challenge_" + parameters.action, p);
687
- // Reload list of challenges from server
688
- this.loadChallengeList();
689
- if (this.userListCriteria)
690
- // We are showing a user list: reload it
691
- this.loadUserList(this.userListCriteria, false);
692
- }
693
- } catch(e) {
694
- // A future TODO is to indicate an error in the UI
695
- }
696
- }
697
-
698
- async markFavorite(userId: string, status: boolean) {
699
- // Mark or de-mark a user as a favorite
700
- try {
701
- await request({
702
- method: "POST",
703
- url: "/favorite",
704
- body: { destuser: userId, action: status ? "add" : "delete" }
705
- });
706
- } catch(e) {
707
- // No need to do anything here - a future TODO is to indicate an error in the UI
708
- }
709
- }
710
-
711
- async cancelFriendship() {
712
- // Cancel the current user as a friend
713
- const user = this.user;
714
- const state = this.state;
715
- if (!user || !state) return;
716
- try {
717
- const json: { ok: boolean; } = await request({
718
- method: "POST",
719
- url: "/cancelplan",
720
- body: { }
721
- });
722
- if (json?.ok) {
723
- // Successfully cancelled: immediately update the friend and hasPaid state
724
- user.friend = false;
725
- state.hasPaid = false;
726
- state.plan = "";
727
- // Log a friendship cancellation event
728
- logEvent("cancel_plan",
729
- {
730
- userid: state.userId,
731
- locale: state.locale,
732
- // Add plan identifiers here
733
- plan: "friend"
734
- }
735
- );
736
- return true;
737
- }
738
- } catch(e) {
739
- // No need to do anything here - a future TODO is to indicate an error in the UI
740
- }
741
- return false;
742
- }
743
-
744
- addChatMessage(game: string, from_userid: string, msg: string, ts: string): boolean {
745
- // Add a chat message to the game's chat message list
746
- if (this.game && this.game.uuid == game) {
747
- const userId = this.state?.userId ?? "";
748
- this.game.addChatMessage(from_userid, msg, ts, from_userid == userId);
749
- // Returning true triggers a redraw
750
- return true;
751
- }
752
- return false;
753
- }
754
-
755
- handleUserMessage(json: any, firstAttach: boolean) {
756
- // Handle an incoming Firebase user message, i.e. a message
757
- // on the /user/[userid] path
758
- if (firstAttach || !this.state)
759
- return;
760
- let redraw = false;
761
- if (json.friend !== undefined) {
762
- // Potential change of user friendship status
763
- const newFriend = json.friend ? true : false;
764
- if (this.user && this.user.friend != newFriend) {
765
- this.user.friend = newFriend;
766
- redraw = true;
767
- }
768
- }
769
- if (json.plan !== undefined) {
770
- // Potential change of user subscription plan
771
- if (this.state.plan != json.plan) {
772
- this.state.plan = json.plan;
773
- redraw = true;
774
- }
775
- if (this.user && !this.user.friend && this.state.plan == "friend") {
776
- // plan == "friend" implies that user.friend should be true
777
- this.user.friend = true;
778
- redraw = true;
779
- }
780
- if (this.state.plan == "" && this.user?.friend) {
781
- // Conversely, an empty plan string means that the user is not a friend
782
- this.user.friend = false;
783
- redraw = true;
784
- }
785
- }
786
- if (json.hasPaid !== undefined) {
787
- // Potential change of payment status
788
- const newHasPaid = (this.state.plan != "" && json.hasPaid) ? true : false;
789
- if (this.state.hasPaid != newHasPaid) {
790
- this.state.hasPaid = newHasPaid;
791
- redraw = true;
792
- }
793
- }
794
- let invalidateGameList = false;
795
- // The following code is a bit iffy since both json.challenge and json.move
796
- // are included in the same message on the /user/[userid] path.
797
- // !!! FIXME: Split this into two separate listeners,
798
- // !!! one for challenges and one for moves
799
- if (json.challenge) {
800
- // Reload challenge list
801
- this.loadChallengeList();
802
- if (this.userListCriteria)
803
- // We are showing a user list: reload it
804
- this.loadUserList(this.userListCriteria, false);
805
- // Reload game list
806
- // !!! FIXME: It is strictly speaking not necessary to reload
807
- // !!! the game list unless this is an acceptance of a challenge
808
- // !!! (issuance or rejection don't cause the game list to change)
809
- invalidateGameList = true;
810
- } else if (json.move) {
811
- // A move has been made in one of this user's games:
812
- // invalidate the game list (will be loaded upon next display)
813
- invalidateGameList = true;
814
- }
815
- if (invalidateGameList && !this.loadingGameList) {
816
- this.gameList = null;
817
- redraw = true;
818
- }
819
- if (redraw)
820
- m.redraw();
821
- }
822
-
823
- handleMoveMessage(json: ServerGame, firstAttach: boolean) {
824
- // Handle an incoming Firebase move message
825
- if (!firstAttach && this.game) {
826
- this.game.update(json);
827
- m.redraw();
828
- }
829
- }
830
-
831
- notifyMove() {
832
- // A move has been made in the game:
833
- // invalidate the game list, since it may have changed
834
- if (!this.loadingGameList) {
835
- this.gameList = null;
836
- }
837
- }
838
-
839
- moreGamesAllowed(): boolean {
840
- // Return true if the user is allowed to have more games ongoing
841
- if (!this.state)
842
- return false;
843
- if (this.loadingGameList)
844
- return false;
845
- if (!this.gameList)
846
- return true;
847
- const numGames = this.gameList.length;
848
- if (numGames >= MAX_GAMES)
849
- return false;
850
- if (this.state.hasPaid)
851
- return true;
852
- return this.gameList.length < this.maxFreeGames;
853
- }
854
-
855
- } // class Model