annotate default/static/websocket.js @ 85:05b500bd6235

chat: chat implementation, first draft: this chat use the new dynamic pages feature. Updates are pushed directly by server. Identities are used to retrieve avatar, and first letter of nickname is used to generate an avatar is none is found (temporary, a more elaborate avatar generation should follow in the future). Scroll is done automatically when new messages arrive, except if scroll is not at the end, as it probably means that user is checking history. User can resize text area and use [shift] + [enter] to enter multi-line messages. History will then scroll to bottom after message has been sent.
author Goffi <goffi@goffi.org>
date Wed, 03 Jan 2018 01:12:16 +0100
parents b2ef34e602cf
children 27d6453a6209
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
84
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
1 /*
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
2 SàT templates: suit of templates for Salut à Toi
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
3 Copyright (C) 2017 Jérôme Poisson (goffi@goffi.org)
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
4
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
5 This program is free software: you can redistribute it and/or modify
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
6 it under the terms of the GNU Affero General Public License as published by
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
7 the Free Software Foundation, either version 3 of the License, or
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
8 (at your option) any later version.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
9
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
10 This program is distributed in the hope that it will be useful,
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
13 GNU Affero General Public License for more details.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
14
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
15 You should have received a copy of the GNU Affero General Public License
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
17 */
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
18
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
19 /* websocket handler */
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
20
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
21
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
22 //TODO: retry websocket instead of reload
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
23 function WSHandler(url, token, debug=false) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
24 var socket = new WebSocket(url, 'libervia_page_' + token );
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
25 var retried = 0;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
26
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
27 var errorHandler = function(error) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
28 if (retried > 20) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
29 console.error("Too many tries, can't start websocket");
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
30 alert("Dynamic connection with server can't be established, please try to reload this page in a while or contact your service administrator");
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
31 return;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
32 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
33 var delay = Math.floor((Math.random() * 10) + 1) + 30 * Math.min(retried, 6);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
34 notifyRetry(delay, function() {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
35 retried++;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
36 socket = new WebSocket(url, 'libervia_page_' + token );
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
37 socket.addEventListener('error', errorHandler);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
38 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
39 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
40
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
41 socket.addEventListener('error', errorHandler);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
42
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
43 if (debug) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
44 socket.addEventListener('message', function(event) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
45 console.log('WS in <== ', JSON.parse(event.data));
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
46 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
47 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
48
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
49 socket.addEventListener('message', function(event) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
50 try {data = JSON.parse(event.data);}
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
51 catch (e) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
52 console.warn('invalid websocket message received: %s', e);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
53 return;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
54 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
55 switch (data.type) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
56 case 'reload':
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
57 location.reload(true);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
58 break;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
59 case 'dom':
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
60 selected_element = document.body.querySelector(data.selectors);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
61 switch (data.update_type) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
62 case 'append':
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
63 var template = document.createElement('template');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
64 template.innerHTML = data.html;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
65 new_element = template.content.firstChild;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
66 selected_element.appendChild(new_element);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
67 break;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
68 default:
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
69 console.warn('Unknown DOM update type: %s', data.update_type);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
70 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
71 break;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
72 default:
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
73 console.warn('Unknown data type: %s', data.type);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
74 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
75 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
76
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
77 socket.addEventListener('open', function (event) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
78 console.log('Websocket opened');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
79 retried = 0;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
80 }.bind(this));
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
81
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
82 this.send = function(data) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
83 if (debug) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
84 console.log('WS out ==> ', data);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
85 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
86 socket.send(JSON.stringify(data));
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
87 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
88
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
89 function notifyRetry(timeout, retryCb) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
90 /* Show a notification dialog informing the user that server can't be reach
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
91 * and call retryCb after timeout seconds.
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
92 * A "retry now" link allows to retry immediately"
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
93 *
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
94 * @param timeout(int): delay before retrying, in seconds
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
95 * @param retryCb(function): function to call when retrying
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
96 */
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
97 var startTime = Date.now() / 1000;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
98 var retryIntervalID;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
99 var notif = document.createElement("div");
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
100 notif.setAttribute('class', 'notification retry');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
101 //FIXME: we use English without translation for now, must be changed when we can use gettext in Libervia pages
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
102 notif.innerHTML = "<p>Can't reach the server, retrying in <span id='retry_counter'></span> seconds</p><p><a id='retry_now'>retry now</a></p>";
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
103 document.body.appendChild(notif);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
104 var retryCounter = document.getElementById('retry_counter');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
105 retryCounter.textContent = timeout;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
106
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
107 var retry = function () {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
108 clearInterval(retryIntervalID);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
109 notif.parentNode.removeChild(notif);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
110 retryCb();
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
111 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
112
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
113 var updateTimer = function () {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
114 var elapsed = Math.floor(Date.now() / 1000 - startTime);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
115 var remaining = timeout - elapsed;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
116 if (remaining < 0) {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
117 retry();
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
118 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
119 else {
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
120 retryCounter.textContent = remaining;
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
121 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
122 };
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
123
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
124 var retryNow = document.getElementById('retry_now');
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
125 retryNow.addEventListener('click', function(){
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
126 retry();
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
127 });
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
128
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
129 retryIntervalID = setInterval(updateTimer, 1000);
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
130 }
b2ef34e602cf base, js (websocket), css (main style): dynamic pages implementation, first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
131 }