[mod] start support for multiple spots
Christian Fraß

Christian Fraß commited on 2021-11-19 22:13:06
Zeige 3 geänderte Dateien mit 274 Einfügungen und 124 Löschungen.

... ...
@@ -36,10 +36,11 @@
36 36
 			</div>
37 37
 			<div id="middle">
38 38
 				<ul class="pane" id="users"></ul>
39
-				<ul class="pane" id="events"></ul>
39
+				<ul class="pane" id="entries"></ul>
40
+				<ul class="pane" id="spots"></ul>
40 41
 			</div>
41 42
 			<form action="?">
42
-				<input type="text" id="message" placeholder="…"/>
43
+				<input type="text" id="content" placeholder="…"/>
43 44
 				<input type="submit" id="send" value="send"/>
44 45
 			</form>
45 46
 		</div>
... ...
@@ -24,14 +24,14 @@ function get_usercolor(name: string): string
24 24
 }
25 25
 
26 26
 
27
-async function backend_call(action: string, data: any): Promise<any>
27
+async function backend_call(connection_id: (null | string), action: string, data: any): Promise<any>
28 28
 {
29 29
 	const response = await fetch
30 30
 	(
31 31
 		`${_conf["backend"]["scheme"]}://${_conf["backend"]["host"]}:${_conf["backend"]["port"].toFixed(0)}/${_conf["backend"]["path"]}`,
32 32
 		{
33 33
 			"method": "POST",
34
-			"body": JSON.stringify({"action": action, "id": _model.connection_id, "data": data}),
34
+			"body": JSON.stringify({"id": connection_id, "action": action, "data": data}),
35 35
 		}
36 36
 	);
37 37
 	if (response.ok)
... ...
@@ -53,118 +53,209 @@ enum enum_state
53 53
 }
54 54
 
55 55
 
56
-type type_model =
56
+type type_event =
57 57
 {
58
-	state: enum_state;
59
-	channel: (null | string);
60
-	nickname: (null | string);
61
-	connection_id: (null | string);
62
-	usershash: (null | string);
58
+	timestamp: int;
59
+	kind: string;
60
+	data: any;
63 61
 };
64 62
 
65 63
 
66
-var _model: type_model =
64
+type type_entry =
67 65
 {
68
-	"state": enum_state.offline,
69
-	"channel": null,
70
-	"nickname": null,
71
-	"connection_id": null,
72
-	"usershash": null,
66
+	timestamp: int;
67
+	sender: string;
68
+	content: string;
73 69
 };
74 70
 
75 71
 
76
-function update_state(): void
72
+type type_user =
77 73
 {
78
-	document.querySelector("body").setAttribute("class", _model.state);
79
-}
74
+	name: string;
75
+	role: string;
76
+};
80 77
 
81 78
 
82
-function update_events(events): void
79
+type type_channel =
83 80
 {
84
-	let dom_events: HTMLUListElement = document.querySelector("#events");
85
-	for (const event of events)
81
+	users: Array<type_user>;
82
+	entries: Array<type_entry>;
83
+};
84
+
85
+
86
+type type_query =
86 87
 {
87
-		const timestring: string = (new Date(event["timestamp"]*1000)).toISOString().slice(11, 19);
88
-		let dom_event: HTMLLIElement = document.createElement("li");
89
-		dom_event.classList.add("event");
90
-		switch (event["kind"])
88
+	entries: Array<type_entry>;
89
+};
90
+
91
+
92
+type type_model =
93
+{
94
+	state: enum_state;
95
+	connection_id: (null | string);
96
+	nickname: (null | string);
97
+	channels: Record<string, type_channel>;
98
+	queries: Record<string, type_query>;
99
+	active: (null | {kind: string, name: string});
100
+};
101
+
102
+
103
+
104
+function model_set_state(model: type_model, state: enum_state): void
105
+{
106
+	model.state = state;
107
+	view_update_state(model);
108
+}
109
+
110
+
111
+function model_process_event(model: type_model, event: type_event): void
112
+{
113
+	switch (event.kind)
91 114
 	{
92 115
 		default:
93 116
 		{
94
-				dom_event.textContent = ("-- unhandled event: " + JSON.stringify(event));
117
+			console.warn("unhandled event kind: " + event.kind);
95 118
 			break;
96 119
 		}
97
-			case "private_message":
98
-			{
120
+		case "userlist":
99 121
 		{
100
-					let dom_time: HTMLSpanElement = document.createElement("span");
101
-					dom_time.classList.add("event_time");
102
-					dom_time.textContent = timestring;
103
-					dom_event.appendChild(dom_time);
122
+			model.channels[event.data["channel"]].users = event.data["users"];
123
+			break;
104 124
 		}
125
+		case "message_channel":
105 126
 		{
106
-					let dom_type: HTMLSpanElement = document.createElement("span");
107
-					dom_type.classList.add("event_type");
108
-					dom_type.textContent = 'private';
109
-					dom_event.appendChild(dom_type);
127
+			model.channels[event.data["channel"]].entries.push
128
+			({
129
+				"timestamp": event.timestamp,
130
+				"sender": event.data["sender"],
131
+				"content": event.data["content"],
132
+			});
133
+			break;
110 134
 		}
135
+		case "message_query":
111 136
 		{
112
-					let dom_sender: HTMLSpanElement = document.createElement("span");
113
-					dom_sender.classList.add("event_sender");
114
-					dom_sender.style.color = get_usercolor(event["data"]["from"] ?? "");
115
-					dom_sender.textContent = event["data"]["from"];
116
-					dom_event.appendChild(dom_sender);
137
+			if (! model.queries.hasOwnProperty(event.data["sender"]))
138
+			{
139
+				model.queries[event.data["sender"]] = {"entries": []};
117 140
 			}
141
+			else
118 142
 			{
119
-					let dom_message: HTMLSpanElement = document.createElement("span");
120
-					dom_message.classList.add("event_message");
121
-					dom_message.textContent = event["data"]["message"];
122
-					dom_event.appendChild(dom_message);
143
+				// do nothing
123 144
 			}
145
+			model.queries[event.data["sender"]].entries.push
146
+			({
147
+				"timestamp": event.timestamp,
148
+				"sender": event.data["sender"],
149
+				"content": event.data["content"],
150
+			});
124 151
 			break;
125 152
 		}
126
-			case "channel_message":
127
-			{
153
+	}
154
+}
155
+
156
+
157
+function view_update_state(model: type_model): void
128 158
 {
129
-					let dom_time: HTMLSpanElement = document.createElement("span");
130
-					dom_time.classList.add("event_time");
131
-					dom_time.textContent = timestring;
132
-					dom_event.appendChild(dom_time);
159
+	document.querySelector("body").setAttribute("class", model.state);
133 160
 }
161
+
162
+
163
+function view_update_spots(model: type_model): void
134 164
 {
135
-					let dom_sender: HTMLSpanElement = document.createElement("span");
136
-					dom_sender.classList.add("event_sender");
137
-					dom_sender.style.color = get_usercolor(event["data"]["from"] ?? "");
138
-					dom_sender.textContent = event["data"]["from"];
139
-					dom_event.appendChild(dom_sender);
165
+	let dom_spots: HTMLUListElement = document.querySelector("#spots");
166
+	const spots: Array<{kind: string; name: string}> =  (
167
+		[]
168
+		.concat(Object.keys(model.channels).map((name) => ({"kind": "channel", "name": name})))
169
+		.concat(Object.keys(model.queries).map((name) => ({"kind": "channel", "name": name})))
170
+	);
171
+	dom_spots.textContent = "";
172
+	for (const spot of spots)
173
+	{
174
+		let dom_spot: HTMLLIElement = document.createElement("li");
175
+		dom_spot.classList.add("spot");
176
+		{
177
+			let dom_name: HTMLSpanElement = document.createElement("span");
178
+			dom_name.classList.add("spot_sender");
179
+			dom_name.textContent = spot.name;
180
+			dom_spot.appendChild(dom_name);
181
+		}
182
+		dom_spots.appendChild(dom_spot);
140 183
 	}
184
+}
185
+
186
+
187
+function view_update_entries(model: type_model): void
188
+{
189
+	let dom_entries: HTMLUListElement = document.querySelector("#entries");
190
+	let source: Array<type_entry>;
191
+	switch (model.active.kind)
141 192
 	{
142
-					let dom_message: HTMLSpanElement = document.createElement("span");
143
-					dom_message.classList.add("event_message");
144
-					dom_message.textContent = event["data"]["message"];
145
-					dom_event.appendChild(dom_message);
193
+		case "channel":
194
+		{
195
+			source = model.channels[model.active.name].entries;
196
+			break;
146 197
 		}
198
+		case "query":
199
+		{
200
+			source = model.queries[model.active.name].entries;
147 201
 			break;
148 202
 		}
149 203
 	}
150
-		dom_events.appendChild(dom_event);
204
+	dom_entries.textContent = "";
205
+	for (const entry of model.channels[model.active.name].entries)
206
+	{
207
+		let dom_entry: HTMLLIElement = document.createElement("li");
208
+		dom_entry.classList.add("entry");
209
+		{
210
+			let dom_time: HTMLSpanElement = document.createElement("span");
211
+			dom_time.classList.add("entry_time");
212
+			dom_time.textContent = (new Date(entry.timestamp*1000)).toISOString().slice(11, 19);
213
+			dom_entry.appendChild(dom_time);
151 214
 		}
152
-	if (events.length > 0)
153 215
 		{
154
-		dom_events.scrollTo(0, dom_events["scrollTopMax"]);
216
+			let dom_sender: HTMLSpanElement = document.createElement("span");
217
+			dom_sender.classList.add("entry_sender");
218
+			dom_sender.style.color = get_usercolor(entry.sender);
219
+			dom_sender.textContent = entry.sender;
220
+			dom_entry.appendChild(dom_sender);
155 221
 		}
156
-	else
157 222
 		{
158
-		// do nothing
223
+			let dom_content: HTMLSpanElement = document.createElement("span");
224
+			dom_content.classList.add("entry_content");
225
+			dom_content.textContent = entry.content;
226
+			dom_entry.appendChild(dom_content);
159 227
 		}
228
+		dom_entries.appendChild(dom_entry);
229
+	}
230
+	dom_entries.scrollTo(0, dom_entries["scrollTopMax"]);
160 231
 }
161 232
 
162 233
 
163
-function update_users(users: Array<{name: string; role: string;}>): void
234
+function view_update_users(model: type_model): void
164 235
 {
165 236
 	let dom_users: HTMLUListElement = document.querySelector("#users");
166 237
 	dom_users.textContent = "";
167
-	const users_sorted: Array<{name: string; role: string;}> = users.sort
238
+	let source: Array<type_user>;
239
+	switch (model.active.kind)
240
+	{
241
+		default:
242
+		{
243
+			console.warn("unhandled kind: " + model.active.kind);
244
+			source = [];
245
+			break;
246
+		}
247
+		case "channel":
248
+		{
249
+			source = model.channels[model.active.name].users;
250
+			break;
251
+		}
252
+		case "query":
253
+		{
254
+			source = [{"name": model.nickname, "role": ""}, {"name": model.active.name, "role": ""}];
255
+			break;
256
+		}
257
+	}
258
+	const users_sorted: Array<type_user> = source.sort
168 259
 	(
169 260
 		(x, y) =>
170 261
 		(
... ...
@@ -197,14 +288,7 @@ function update_users(users: Array<{name: string; role: string;}>): void
197 288
 }
198 289
 
199 290
 
200
-function set_state(state: enum_state): void
201
-{
202
-	_model.state = state;
203
-	update_state();
204
-}
205
-
206
-
207
-function setup_view(): void
291
+function view_setup(model: type_model): void
208 292
 {
209 293
 	document.querySelector<HTMLInputElement>("#channel").value = _conf["irc"]["predefined_channel"];
210 294
 	document.querySelector<HTMLInputElement>("#nickname").value = (_conf["irc"]["predefined_nickname_prefix"] + (Math.random()*100).toFixed(0));
... ...
@@ -212,11 +296,11 @@ function setup_view(): void
212 296
 	(
213 297
 		async () =>
214 298
 		{
215
-			switch (_model.state)
299
+			switch (model.state)
216 300
 			{
217 301
 				default:
218 302
 				{
219
-					throw (new Error("invalid state: " + _model.state));
303
+					throw (new Error("invalid state: " + model.state));
220 304
 					break;
221 305
 				}
222 306
 				case enum_state.offline:
... ...
@@ -226,10 +310,10 @@ function setup_view(): void
226 310
 				}
227 311
 				case enum_state.connecting:
228 312
 				{
229
-					const ready: boolean = await backend_call("check", null);
313
+					const ready: boolean = await backend_call(model.connection_id, "check", null);
230 314
 					if (ready)
231 315
 					{
232
-						set_state(enum_state.online);
316
+						model_set_state(model, enum_state.online);
233 317
 					}
234 318
 					else
235 319
 					{
... ...
@@ -239,29 +323,25 @@ function setup_view(): void
239 323
 				}
240 324
 				case enum_state.online:
241 325
 				{
242
-					const stuff: any = await backend_call("fetch", null);
243
-					update_events(stuff["events"]);
244
-					const usershash: string = btoa(JSON.stringify(stuff["users"]));
245
-					if (_model.usershash !== usershash)
246
-					{
247
-						_model.usershash = usershash;
248
-						update_users(stuff["users"]);
249
-					}
250
-					else
326
+					const events: Array<type_event> = await backend_call(model.connection_id, "fetch", null);
327
+					for (const event of events)
251 328
 					{
252
-						// do nothing
329
+						model_process_event(model, event);
253 330
 					}
331
+					view_update_users(model);
332
+					view_update_entries(model);
333
+					view_update_spots(model);
254 334
 					break;
255 335
 				}
256 336
 			}
257 337
 		},
258 338
 		_conf["settings"]["poll_interval_in_milliseconds"]
259 339
 	);
260
-	set_state(enum_state.offline);
340
+	model_set_state(model, enum_state.offline);
261 341
 }
262 342
 
263 343
 
264
-function setup_control(): void
344
+function control_setup(model: type_model): void
265 345
 {
266 346
 	document.querySelector("#connect > form").addEventListener
267 347
 	(
... ...
@@ -269,23 +349,36 @@ function setup_control(): void
269 349
 		async (event) =>
270 350
 		{
271 351
 			event.preventDefault();
352
+			
353
+			model_set_state(model, enum_state.connecting);
354
+			
272 355
 			let dom_nickname: HTMLInputElement = document.querySelector<HTMLInputElement>("#nickname");
273 356
 			let dom_channel: HTMLInputElement = document.querySelector<HTMLInputElement>("#channel");
274 357
 			const nickname: string = dom_nickname.value;
275
-			const channel: string = dom_channel.value;
358
+			const channel_names: Array<string> = dom_channel.value.split(",");
359
+			
276 360
 			const connection_id: string = await backend_call
277 361
 			(
362
+				model.connection_id,
278 363
 				"connect",
279 364
 				{
280 365
 					"server": _conf["irc"]["server"],
281
-					"channels": [channel],
366
+					"channels": channel_names,
282 367
 					"nickname": nickname,
283 368
 				}
284 369
 			);
285
-			_model.connection_id = connection_id;
286
-			_model.channel = channel;
287
-			_model.nickname = nickname;
288
-			set_state(enum_state.connecting);
370
+			model.connection_id = connection_id;
371
+			model.nickname = nickname;
372
+			for (const channel_name of channel_names)
373
+			{
374
+				model.channels[channel_name] =
375
+				{
376
+					"users": [],
377
+					"entries": [],
378
+				};
379
+			}
380
+			// TODO: can crash
381
+			model.active = {"kind": "channel", "name": channel_names[0]};
289 382
 		}
290 383
 	);
291 384
 	document.querySelector("#disconnect").addEventListener
... ...
@@ -295,42 +388,80 @@ function setup_control(): void
295 388
 		{
296 389
 			await backend_call
297 390
 			(
391
+				model.connection_id,
298 392
 				"disconnect",
299 393
 				null
300 394
 			);
301
-			set_state(enum_state.offline);
302
-			_model.connection_id = null;
395
+			model_set_state(model, enum_state.offline);
396
+			model.connection_id = null;
303 397
 		}
304 398
 	);
305 399
 	document.querySelector("#main > form").addEventListener
306 400
 	(
307 401
 		"submit",
308
-		async (event) =>
402
+		async (e) =>
309 403
 		{
310 404
 			event.preventDefault();
311
-			let dom_message: HTMLInputElement = document.querySelector<HTMLInputElement>("#message");
312
-			const message: string = dom_message.value;
313
-			dom_message.value = "";
314
-			dom_message.focus();
315
-			const event_: any =
405
+			
406
+			let dom_content: HTMLInputElement = document.querySelector<HTMLInputElement>("#content");
407
+			const content: string = dom_content.value;
408
+			dom_content.value = "";
409
+			dom_content.focus();
410
+			switch (model.active.kind)
411
+			{
412
+				case "channel":
413
+				{
414
+					const event: type_event =
316 415
 					{
317 416
 						"timestamp": get_timestamp(),
318
-				"kind": "channel_message",
319
-				"data": {
320
-					"from": _model.nickname,
321
-					"to": _model.channel,
322
-					"message": message,
417
+						"kind": "message_channel",
418
+						"data":
419
+						{
420
+							"channel": model.active.name,
421
+							"sender": model.nickname,
422
+							"content": content,
323 423
 						}
324 424
 					};
325
-			update_events([event_]);
326
-			await backend_call
425
+					model_process_event(model, event);
426
+					view_update_entries(model);
427
+					backend_call
327 428
 					(
328
-				"send",
429
+						model.connection_id,
430
+						"send_channel",
431
+						{
432
+							"channel": model.active.name,
433
+							"content": content,
434
+						}
435
+					);
436
+					break;
437
+				}
438
+				case "query":
329 439
 				{
330
-					"channel": _model.channel,
440
+					/*
441
+					const event: type_event =
442
+					{
443
+						"timestamp": get_timestamp(),
444
+						"kind": "message_query",
445
+						"data":
446
+						{
447
+							"sender": model.nickname,
331 448
 							"message": message,
332 449
 						}
450
+					};
451
+					model_process_event(model, event);
452
+					 */
453
+					backend_call
454
+					(
455
+						model.connection_id,
456
+						"send_query",
457
+						{
458
+							"receiver": model.active.name,
459
+							"content": content,
460
+						}
333 461
 					);
462
+					break;
463
+				}
464
+			}
334 465
 		}
335 466
 	);
336 467
 }
... ...
@@ -339,8 +470,17 @@ function setup_control(): void
339 470
 async function main(): Promise<void>
340 471
 {
341 472
 	_conf = await fetch("conf.json").then(x => x.json());
342
-	setup_view();
343
-	setup_control();
473
+	const model: type_model =
474
+	{
475
+		"state": enum_state.offline,
476
+		"connection_id": null,
477
+		"nickname": null,
478
+		"channels": {},
479
+		"queries": {},
480
+		"active": null,
481
+	};
482
+	view_setup(model);
483
+	control_setup(model);
344 484
 }
345 485
 
346 486
 
... ...
@@ -41,12 +41,12 @@ label
41 41
 	margin: 16px 0;
42 42
 }
43 43
 
44
-.event > *
44
+.entry > *
45 45
 {
46 46
 	display: inline-block;
47 47
 }
48 48
 
49
-.event_time
49
+.entry_time
50 50
 {
51 51
 	margin: 0 4px;
52 52
 	
... ...
@@ -63,7 +63,7 @@ label
63 63
 	}
64 64
 }
65 65
 
66
-.event_type
66
+.entry_type
67 67
 {
68 68
 	margin: 0 4px;
69 69
 	
... ...
@@ -80,7 +80,7 @@ label
80 80
 	}
81 81
 }
82 82
 
83
-.event_sender
83
+.entry_sender
84 84
 {
85 85
 	margin: 0 4px;
86 86
 	
... ...
@@ -167,7 +167,16 @@ body
167 167
 	flex-direction: row-reverse;
168 168
 	flex-wrap: wrap;
169 169
 	
170
-	& #events
170
+	& #spots
171
+	{
172
+		flex-grow: 1;
173
+		flex-shrink: 1;
174
+		
175
+		min-width: 240px;
176
+		height: 75vh;
177
+	}
178
+	
179
+	& #entries
171 180
 	{
172 181
 		flex-grow: 4;
173 182
 		flex-shrink: 4;
... ...
@@ -186,7 +195,7 @@ body
186 195
 	}
187 196
 }
188 197
 
189
-#message
198
+#content
190 199
 {
191 200
 	border: none;
192 201
 	
... ...
@@ -203,7 +212,7 @@ body
203 212
 	flex-direction: row;
204 213
 	flex-wrap: wrap;
205 214
 	
206
-	& > #message {flex: 7;}
215
+	& > #content {flex: 7;}
207 216
 	& > #send {flex: 1;}
208 217
 }
209 218
 
210 219