@mideind/netskrafl-react 1.0.0-beta.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.
Files changed (68) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.json +13 -0
  3. package/README.md +63 -0
  4. package/dist/cjs/index.css +6837 -0
  5. package/dist/cjs/index.js +3046 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/esm/index.css +6837 -0
  8. package/dist/esm/index.js +3046 -0
  9. package/dist/esm/index.js.map +1 -0
  10. package/dist/types.d.ts +41 -0
  11. package/package.json +63 -0
  12. package/rollup.config.js +60 -0
  13. package/src/components/index.ts +2 -0
  14. package/src/components/netskrafl/Netskrafl.stories.tsx +66 -0
  15. package/src/components/netskrafl/Netskrafl.tsx +135 -0
  16. package/src/components/netskrafl/Netskrafl.types.ts +7 -0
  17. package/src/components/netskrafl/index.ts +2 -0
  18. package/src/css/fonts.css +4 -0
  19. package/src/css/glyphs.css +224 -0
  20. package/src/css/skrafl-explo.css +6616 -0
  21. package/src/fonts/glyphicons-regular.eot +0 -0
  22. package/src/fonts/glyphicons-regular.ttf +0 -0
  23. package/src/fonts/glyphicons-regular.woff +0 -0
  24. package/src/index.ts +2 -0
  25. package/src/messages/messages.json +1576 -0
  26. package/src/mithril/actions.ts +319 -0
  27. package/src/mithril/bag.ts +65 -0
  28. package/src/mithril/bestdisplay.ts +74 -0
  29. package/src/mithril/blankdialog.ts +94 -0
  30. package/src/mithril/board.ts +336 -0
  31. package/src/mithril/buttons.ts +303 -0
  32. package/src/mithril/challengedialog.ts +186 -0
  33. package/src/mithril/channel.ts +162 -0
  34. package/src/mithril/chat.ts +228 -0
  35. package/src/mithril/components.ts +496 -0
  36. package/src/mithril/dragdrop.ts +219 -0
  37. package/src/mithril/elopage.ts +180 -0
  38. package/src/mithril/friend.ts +227 -0
  39. package/src/mithril/game.ts +1378 -0
  40. package/src/mithril/gameview.ts +111 -0
  41. package/src/mithril/globalstate.ts +33 -0
  42. package/src/mithril/i18n.ts +186 -0
  43. package/src/mithril/localstorage.ts +133 -0
  44. package/src/mithril/login.ts +122 -0
  45. package/src/mithril/logo.ts +270 -0
  46. package/src/mithril/main.ts +737 -0
  47. package/src/mithril/mithril.ts +29 -0
  48. package/src/mithril/model.ts +817 -0
  49. package/src/mithril/movelistitem.ts +226 -0
  50. package/src/mithril/page.ts +852 -0
  51. package/src/mithril/playername.ts +91 -0
  52. package/src/mithril/promodialog.ts +82 -0
  53. package/src/mithril/recentlist.ts +148 -0
  54. package/src/mithril/request.ts +52 -0
  55. package/src/mithril/review.ts +634 -0
  56. package/src/mithril/rightcolumn.ts +398 -0
  57. package/src/mithril/searchbutton.ts +118 -0
  58. package/src/mithril/statsdisplay.ts +109 -0
  59. package/src/mithril/tabs.ts +169 -0
  60. package/src/mithril/tile.ts +145 -0
  61. package/src/mithril/twoletter.ts +76 -0
  62. package/src/mithril/types.ts +379 -0
  63. package/src/mithril/userinfodialog.ts +171 -0
  64. package/src/mithril/util.ts +304 -0
  65. package/src/mithril/wait.ts +246 -0
  66. package/src/mithril/wordcheck.ts +102 -0
  67. package/tsconfig.json +28 -0
  68. package/vite.config.ts +12 -0
@@ -0,0 +1,186 @@
1
+ /*
2
+
3
+ ChallengeDialog.ts
4
+
5
+ Challenge dialog component
6
+
7
+ Copyright (C) 2024 Miðeind ehf.
8
+ Author: Vilhjalmur Thorsteinsson
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
+ */
15
+
16
+ import { IView, UserListItem } from "./types";
17
+ import { ComponentFunc, m } from "./mithril";
18
+ import { mt, t, ts } from "./i18n";
19
+ import { glyph, nbsp } from "./util";
20
+ import { DialogButton, MultiSelection, OnlinePresence } from "./components";
21
+
22
+ interface IAttributes {
23
+ view: IView;
24
+ item: UserListItem;
25
+ }
26
+
27
+ export const ChallengeDialog: ComponentFunc<IAttributes> = () => {
28
+ // Show a dialog box for a new challenge being issued
29
+ let manualChallenge = false;
30
+ return {
31
+ view: (vnode) => {
32
+ const { view, item } = vnode.attrs;
33
+ const model = view.model;
34
+ const state = model.state;
35
+ if (!state) return undefined;
36
+ const manual = state.plan !== ""; // If subscriber/friend, allow manual challenges
37
+ const fairPlay = item.fairplay && state.fairPlay; // Both users are fair-play
38
+ return m(".modal-dialog",
39
+ { id: 'chall-dialog', style: { visibility: 'visible' } },
40
+ m(".ui-widget.ui-widget-content.ui-corner-all", { id: 'chall-form' },
41
+ [
42
+ m(".chall-hdr",
43
+ m("table",
44
+ m("tbody",
45
+ m("tr",
46
+ [
47
+ m("td", m("h1.chall-icon", glyph("hand-right"))),
48
+ m("td.l-border",
49
+ [
50
+ m(OnlinePresence, { id: "chall-online", userId: item.userid }),
51
+ m("h1", item.nick),
52
+ m("h2", item.fullname)
53
+ ]
54
+ )
55
+ ]
56
+ )
57
+ )
58
+ )
59
+ ),
60
+ m("div", { style: { "text-align": "center" } },
61
+ [
62
+ m(".promo-fullscreen",
63
+ [
64
+ mt("p", [mt("strong", "Ný áskorun"), " - veldu lengd viðureignar:"]),
65
+ m(MultiSelection,
66
+ { initialSelection: 0, defaultClass: 'chall-time' },
67
+ [
68
+ m("div", { id: 'chall-none', tabindex: 1 },
69
+ t("Viðureign án klukku")
70
+ ),
71
+ m("div", { id: 'chall-10', tabindex: 2 },
72
+ [glyph("time"), t("2 x 10 mínútur")]
73
+ ),
74
+ m("div", { id: 'chall-15', tabindex: 3 },
75
+ [glyph("time"), t("2 x 15 mínútur")]
76
+ ),
77
+ m("div", { id: 'chall-20', tabindex: 4 },
78
+ [glyph("time"), t("2 x 20 mínútur")]
79
+ ),
80
+ m("div", { id: 'chall-25', tabindex: 5 },
81
+ [glyph("time"), t("2 x 25 mínútur")]
82
+ ),
83
+ state?.runningLocal ? // !!! TODO Debugging aid
84
+ m("div", { id: 'chall-3', tabindex: 6 },
85
+ [glyph("time"), t("2 x 3 mínútur")]
86
+ )
87
+ :
88
+ m("div", { id: 'chall-30', tabindex: 6 },
89
+ [glyph("time"), t("2 x 30 mínútur")]
90
+ )
91
+ ]
92
+ )
93
+ ]
94
+ ),
95
+ m(".promo-mobile",
96
+ [
97
+ m("p", mt("strong", "Ný áskorun")),
98
+ m(".chall-time.selected",
99
+ { id: 'extra-none', tabindex: 1 },
100
+ t("Viðureign án klukku")
101
+ )
102
+ ]
103
+ )
104
+ ]
105
+ ),
106
+ manual ? m("div", { id: "chall-manual" },
107
+ [
108
+ mt("span.caption.wide",
109
+ [
110
+ "Nota ", mt("strong", "handvirka véfengingu"),
111
+ m("br"), "(\"keppnishamur\")"
112
+ ]
113
+ ),
114
+ m(".toggler[id='manual-toggler'][tabindex='7']",
115
+ {
116
+ onclick: (ev: Event) => {
117
+ manualChallenge = !manualChallenge;
118
+ ev.preventDefault();
119
+ }
120
+ },
121
+ [
122
+ m(`.option${manualChallenge ? "" : ".selected"}`,
123
+ m("span", nbsp())
124
+ ),
125
+ m(`.option${manualChallenge ? ".selected" : ""}`,
126
+ glyph("lightbulb")
127
+ )
128
+ ]
129
+ )
130
+ ]
131
+ ) : "",
132
+ fairPlay ? m("div", { id: "chall-fairplay" },
133
+ [
134
+ t("Báðir leikmenn lýsa því yfir að þeir skrafla "),
135
+ m("br"),
136
+ mt("strong", "án stafrænna hjálpartækja"),
137
+ t(" af nokkru tagi"), "."
138
+ ]
139
+ ) : "",
140
+ m(DialogButton,
141
+ {
142
+ id: "chall-cancel",
143
+ title: ts("Hætta við"),
144
+ tabindex: 8,
145
+ onclick: (ev: Event) => {
146
+ view.popDialog();
147
+ ev.preventDefault();
148
+ }
149
+ },
150
+ glyph("remove")
151
+ ),
152
+ m(DialogButton,
153
+ {
154
+ id: "chall-ok",
155
+ title: ts("Skora á"),
156
+ tabindex: 9,
157
+ onclick: (ev: Event) => {
158
+ // Issue a new challenge
159
+ let duration: string | number =
160
+ document.querySelector("div.chall-time.selected")?.id.slice(6) ?? "0";
161
+ if (duration === "none")
162
+ duration = 0;
163
+ else
164
+ duration = parseInt(duration);
165
+ item.chall = true;
166
+ model.modifyChallenge(
167
+ {
168
+ destuser: item.userid,
169
+ action: "issue",
170
+ duration: duration,
171
+ fairplay: fairPlay,
172
+ manual: manualChallenge
173
+ }
174
+ );
175
+ view.popDialog();
176
+ ev.preventDefault();
177
+ }
178
+ },
179
+ glyph("ok")
180
+ )
181
+ ]
182
+ )
183
+ );
184
+ }
185
+ };
186
+ };
@@ -0,0 +1,162 @@
1
+ /*
2
+
3
+ Channel.ts
4
+
5
+ Utility functions for working with Firebase
6
+
7
+ Copyright (C) 2024 Miðeind ehf.
8
+ Original 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
+ */
15
+
16
+ import { FirebaseApp, FirebaseOptions, initializeApp } from 'firebase/app';
17
+ import { getAnalytics, logEvent as firebaseLogEvent, Analytics } from 'firebase/analytics';
18
+ import { getAuth, signInWithCustomToken, onAuthStateChanged, Auth } from 'firebase/auth';
19
+ import { Database, getDatabase, off, onDisconnect, onValue, push, ref, remove, set } from 'firebase/database';
20
+
21
+ import { GlobalState } from "./globalstate";
22
+
23
+ let app: FirebaseApp;
24
+ let auth: Auth;
25
+ let database: Database;
26
+ let analytics: Analytics;
27
+
28
+ function initFirebase(state: GlobalState) {
29
+ try {
30
+ const projectId = state.projectId;
31
+ const firebaseOptions: FirebaseOptions = {
32
+ projectId,
33
+ apiKey: state.firebaseAPIKey,
34
+ authDomain: `${projectId}.firebaseapp.com`,
35
+ databaseURL: state.databaseURL,
36
+ storageBucket: `${projectId}.firebasestorage.app`,
37
+ messagingSenderId: state.firebaseSenderId,
38
+ appId: state.firebaseAppId,
39
+ measurementId: state.measurementId,
40
+ };
41
+ app = initializeApp(firebaseOptions, "netskrafl");
42
+ if (!app) {
43
+ console.error("Failed to initialize Firebase");
44
+ }
45
+ } catch (e) {
46
+ console.error("Failed to initialize Firebase", e);
47
+ }
48
+ }
49
+
50
+ export async function loginFirebase(
51
+ state: GlobalState,
52
+ firebaseToken: string,
53
+ onLoginFunc?: () => void
54
+ ) {
55
+ if (!app) initFirebase(state);
56
+ if (!app) return;
57
+ const userId = state.userId;
58
+ const locale = state.locale;
59
+ auth = getAuth(app);
60
+ if (!auth) {
61
+ console.error("Failed to initialize Firebase Auth");
62
+ } else {
63
+ // Register our login function to execute once the user login is done
64
+ onAuthStateChanged(auth,
65
+ (user) => {
66
+ const signedIn = (user !== null);
67
+ if (signedIn) {
68
+ // User is signed in
69
+ onLoginFunc && onLoginFunc();
70
+ // For new users, log an additional signup event
71
+ if (state.newUser) {
72
+ logEvent("sign_up",
73
+ {
74
+ locale: state.locale,
75
+ method: state.loginMethod,
76
+ userid: state.userId
77
+ }
78
+ );
79
+ }
80
+ // And always log a login event
81
+ logEvent("login",
82
+ {
83
+ locale: state.locale,
84
+ method: state.loginMethod,
85
+ userid: state.userId
86
+ }
87
+ );
88
+ } else {
89
+ // No user is signed in
90
+ }
91
+ }
92
+ );
93
+ }
94
+ // Log in to Firebase using the provided custom token
95
+ await signInWithCustomToken(auth, firebaseToken);
96
+ database = getDatabase(app);
97
+ if (!database) {
98
+ console.error("Failed to initialize Firebase Database");
99
+ }
100
+ analytics = getAnalytics(app);
101
+ if (!analytics) {
102
+ console.error("Failed to initialize Firebase Analytics");
103
+ }
104
+ initPresence(state.projectId, userId, locale);
105
+ }
106
+
107
+ function initPresence(projectId: string, userId: string, locale: string) {
108
+ // Ensure that this user connection is recorded in Firebase
109
+ if (!database) return;
110
+ const connectedRef = ref(database, '.info/connected');
111
+ // Create a unique connection entry for this user
112
+ const connectionPath =
113
+ projectId === "netskrafl"
114
+ ? `connection/${userId}`
115
+ : `connection/${locale}/${userId}`;
116
+ const userRef = push(ref(database, connectionPath));
117
+ onValue(connectedRef, (snapshot) => {
118
+ if (snapshot.val()) {
119
+ // We're connected (or reconnected)
120
+ // When this user disconnects, remove this entry
121
+ onDisconnect(userRef).remove();
122
+ // Set presence
123
+ set(userRef, true);
124
+ }
125
+ else
126
+ // Unset presence
127
+ remove(userRef);
128
+ });
129
+ }
130
+
131
+ export function attachFirebaseListener(
132
+ path: string, func: (json: any, firstAttach: boolean) => void
133
+ ) {
134
+ // Attach a message listener to a Firebase path
135
+ if (!database) return;
136
+ let cnt = 0;
137
+ const pathRef = ref(database, path);
138
+ onValue(pathRef, function(snapshot) {
139
+ // Note: we need function() here for proper closure
140
+ // The cnt variable is used to tell the listener whether it's being
141
+ // called upon the first attach or upon a later data change
142
+ cnt++;
143
+ const json = snapshot.val();
144
+ // console.log("received on path", path, ":", json);
145
+ if (json) {
146
+ func(json, cnt == 1);
147
+ }
148
+ });
149
+ }
150
+
151
+ export function detachFirebaseListener(path: string) {
152
+ // Detach a message listener from a Firebase path
153
+ if (!database) return;
154
+ const pathRef = ref(database, path);
155
+ off(pathRef);
156
+ }
157
+
158
+ export function logEvent(ev: string, params: any) {
159
+ // Log a Firebase analytics event
160
+ if (!analytics) return;
161
+ firebaseLogEvent(analytics, ev, params);
162
+ }
@@ -0,0 +1,228 @@
1
+ /*
2
+
3
+ Chat.ts
4
+
5
+ Chat component
6
+
7
+ Copyright (C) 2025 Miðeind ehf.
8
+ Author: Vilhjalmur Thorsteinsson
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
+ */
15
+
16
+ import { IView } from "./types";
17
+ import { ts } from "./i18n";
18
+ import { ComponentFunc, m, VnodeChildren, VnodeDOM } from "./mithril";
19
+ import { escapeHtml, getInput, glyph, setInput } from "./util";
20
+ import { DialogButton } from "./components";
21
+ import { serverUrl } from "./request";
22
+
23
+ // Max number of chat messages per game
24
+ const MAX_CHAT_MESSAGES = 250;
25
+
26
+ interface IAttributes {
27
+ view: IView;
28
+ }
29
+
30
+ const EMOTICONS = [
31
+ { icon: ":-)", image: "/static/icontexto_emoticons_03.png" },
32
+ { icon: ":-D", image: "/static/icontexto_emoticons_02.png" },
33
+ { icon: ";-)", image: "/static/icontexto_emoticons_04.png" },
34
+ { icon: ":-(", image: "/static/icontexto_emoticons_12.png" },
35
+ { icon: ":-o", image: "/static/icontexto_emoticons_10.png" },
36
+ { icon: ":-O", image: "/static/icontexto_emoticons_10.png" },
37
+ { icon: ":-p", image: "/static/icontexto_emoticons_14.png" },
38
+ { icon: ":-P", image: "/static/icontexto_emoticons_14.png" },
39
+ { icon: "B-)", image: "/static/icontexto_emoticons_16.png" },
40
+ { icon: ":)", image: "/static/icontexto_emoticons_03.png" },
41
+ { icon: ":D", image: "/static/icontexto_emoticons_02.png" },
42
+ { icon: ";)", image: "/static/icontexto_emoticons_04.png" },
43
+ { icon: ":(", image: "/static/icontexto_emoticons_12.png" },
44
+ { icon: ":o", image: "/static/icontexto_emoticons_10.png" },
45
+ { icon: ":O", image: "/static/icontexto_emoticons_10.png" },
46
+ { icon: ":p", image: "/static/icontexto_emoticons_14.png" },
47
+ { icon: ":P", image: "/static/icontexto_emoticons_14.png" },
48
+ { icon: "(y)", image: "/static/thumb-up.png" }
49
+ ];
50
+
51
+ export const Chat: ComponentFunc<IAttributes> = (initialVnode) => {
52
+ // The chat tab
53
+
54
+ const { view } = initialVnode.attrs;
55
+ const model = view.model;
56
+ const game = model.game!;
57
+ const state = model.state!;
58
+
59
+ function decodeTimestamp(ts: string) {
60
+ // Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
61
+ return {
62
+ year: parseInt(ts.slice(0, 4)),
63
+ month: parseInt(ts.slice(5, 7)),
64
+ day: parseInt(ts.slice(8, 10)),
65
+ hour: parseInt(ts.slice(11, 13)),
66
+ minute: parseInt(ts.slice(14, 16)),
67
+ second: parseInt(ts.slice(17, 19))
68
+ };
69
+ }
70
+
71
+ function dateFromTimestamp(ts: string) {
72
+ // Create a JavaScript millisecond-based representation of an ISO timestamp
73
+ var dcTs = decodeTimestamp(ts);
74
+ return Date.UTC(dcTs.year, dcTs.month - 1, dcTs.day,
75
+ dcTs.hour, dcTs.minute, dcTs.second);
76
+ }
77
+
78
+ function timeDiff(dtFrom: number, dtTo: number) {
79
+ // Return the difference between two JavaScript time points, in seconds
80
+ return Math.round((dtTo - dtFrom) / 1000.0);
81
+ }
82
+
83
+ let dtLastMsg: number | null = null;
84
+
85
+ function makeTimestamp(ts: string, key: number): VnodeChildren {
86
+ // Decode the ISO format timestamp we got from the server
87
+ let dtTs = dateFromTimestamp(ts);
88
+ let result: VnodeChildren = null;
89
+ if (dtLastMsg === null || timeDiff(dtLastMsg, dtTs) >= 5 * 60) {
90
+ // If 5 minutes or longer interval between messages,
91
+ // insert a time
92
+ const ONE_DAY = 24 * 60 * 60 * 1000; // 24 hours expressed in milliseconds
93
+ const dtNow = new Date().getTime();
94
+ let dtToday = dtNow - dtNow % ONE_DAY; // Start of today (00:00 UTC)
95
+ let dtYesterday = dtToday - ONE_DAY; // Start of yesterday
96
+ let strTs: string;
97
+ if (dtTs < dtYesterday) {
98
+ // Older than today or yesterday: Show full timestamp YYYY-MM-DD HH:MM
99
+ strTs = ts.slice(0, -3);
100
+ } else if (dtTs < dtToday) {
101
+ // Yesterday
102
+ strTs = "Í gær " + ts.slice(11, 16);
103
+ } else {
104
+ // Today
105
+ strTs = ts.slice(11, 16);
106
+ }
107
+ result = m(".chat-ts", { key: key }, strTs);
108
+ }
109
+ dtLastMsg = dtTs;
110
+ return result;
111
+ }
112
+
113
+ const player = game ? game.player : null;
114
+
115
+ function replaceEmoticons(str: string): string {
116
+ // Replace all emoticon shortcuts in the string str with a corresponding image URL
117
+ for (const emoticon of EMOTICONS)
118
+ if (str.indexOf(emoticon.icon) >= 0) {
119
+ // The string contains the emoticon: prepare to replace all occurrences
120
+ const imgUrl = serverUrl(emoticon.image);
121
+ const img = `<img src='${imgUrl}' height='32' width='32'>`;
122
+ // Re the following trick, see https://stackoverflow.com/questions/1144783/
123
+ // replacing-all-occurrences-of-a-string-in-javascript
124
+ str = str.split(emoticon.icon).join(img);
125
+ }
126
+ return str;
127
+ }
128
+
129
+ function chatMessages(): VnodeChildren {
130
+ let r: VnodeChildren = [];
131
+ if (game?.chatLoading || !game?.messages)
132
+ return r;
133
+ var key = 0;
134
+ const userId = model.state?.userId ?? "";
135
+ for (const msg of game.messages) {
136
+ let p = player ?? 0;
137
+ if (msg.from_userid != userId)
138
+ p = 1 - p;
139
+ const mTs = makeTimestamp(msg.ts, key);
140
+ if (mTs !== null) {
141
+ r.push(mTs);
142
+ key++;
143
+ }
144
+ let escMsg = escapeHtml(msg.msg);
145
+ escMsg = replaceEmoticons(escMsg);
146
+ r.push(m(".chat-msg" +
147
+ (p === 0 ? ".left" : ".right") +
148
+ (p === player ? ".local" : ".remote"),
149
+ { key: key++ },
150
+ m.trust(escMsg))
151
+ );
152
+ }
153
+ return r;
154
+ }
155
+
156
+ function scrollChatToBottom(): void {
157
+ // Scroll the last chat message into view
158
+ const chatlist = document.querySelectorAll("#chat-area .chat-msg");
159
+ if (!chatlist.length)
160
+ return;
161
+ const target: HTMLElement = chatlist[chatlist.length - 1] as HTMLElement;
162
+ (target.parentNode as HTMLElement).scrollTop = target.offsetTop;
163
+ }
164
+
165
+ function focus(vnode: VnodeDOM) {
166
+ // Put the focus on the DOM object associated with the vnode
167
+ if (!view.isDialogShown()) {
168
+ // Don't hijack the focus from a dialog overlay
169
+ (vnode.dom as HTMLElement).focus();
170
+ }
171
+ }
172
+
173
+ function sendMessage(): void {
174
+ let msg = getInput("msg").trim();
175
+ if (game && msg.length > 0) {
176
+ game.sendMessage(msg);
177
+ setInput("msg", "");
178
+ }
179
+ }
180
+
181
+ const numMessages = game?.messages ? game.messages.length : 0;
182
+
183
+ return {
184
+ view: () => m(".chat",
185
+ {
186
+ style: "z-index: 6" // Appear on top of board on mobile
187
+ // key: uuid
188
+ },
189
+ [
190
+ m(".chat-area" + (game?.showClock() ? ".with-clock" : ""),
191
+ {
192
+ id: 'chat-area',
193
+ // Make sure that we see the bottom-most chat message
194
+ oncreate: scrollChatToBottom,
195
+ onupdate: scrollChatToBottom
196
+ },
197
+ chatMessages()
198
+ ),
199
+ m(".chat-input",
200
+ [
201
+ m("input.chat-txt",
202
+ {
203
+ type: "text",
204
+ id: "msg",
205
+ name: "msg",
206
+ maxlength: 254,
207
+ disabled: (numMessages >= MAX_CHAT_MESSAGES),
208
+ oncreate: (vnode) => { focus(vnode); },
209
+ onupdate: (vnode) => { focus(vnode); },
210
+ onkeypress: (ev: KeyboardEvent) => {
211
+ if (ev.key == "Enter") { sendMessage(); ev.preventDefault(); }
212
+ }
213
+ }
214
+ ),
215
+ m(DialogButton,
216
+ {
217
+ id: "chat-send",
218
+ title: ts("Senda"),
219
+ onclick: (ev: Event) => { sendMessage(); ev.preventDefault(); }
220
+ },
221
+ glyph("chat"),
222
+ )
223
+ ]
224
+ )
225
+ ]
226
+ )
227
+ }
228
+ };