Christian Fraß commited on 2021-11-20 15:02:57
Zeige 11 geänderte Dateien mit 826 Einfügungen und 543 Löschungen.
| ... | ... |
@@ -32,7 +32,6 @@ A simple IRC client, realized as web application |
| 32 | 32 |
# Plans and ToDos |
| 33 | 33 |
|
| 34 | 34 |
- use websockets instead of polling |
| 35 |
-- support multiple channels and private messages |
|
| 36 | 35 |
- support commands (e.g. `/nick new_name`) |
| 37 | 36 |
- get correct user list (seems like it does not update when a user leaves the room) |
| 38 | 37 |
|
| ... | ... |
@@ -0,0 +1,98 @@ |
| 1 |
+namespace ns_control |
|
| 2 |
+{
|
|
| 3 |
+ |
|
| 4 |
+ /** |
|
| 5 |
+ * ugly hack to prevent multiple listener addition |
|
| 6 |
+ */ |
|
| 7 |
+ function register_listener |
|
| 8 |
+ ( |
|
| 9 |
+ dom_element: HTMLElement, |
|
| 10 |
+ eventname: string, |
|
| 11 |
+ handler: (event)=>void |
|
| 12 |
+ ): void |
|
| 13 |
+ {
|
|
| 14 |
+ const classname: string = `has_listener_for_${eventname}`;
|
|
| 15 |
+ if (! dom_element.classList.contains(classname)) |
|
| 16 |
+ {
|
|
| 17 |
+ dom_element.addEventListener(eventname, handler); |
|
| 18 |
+ dom_element.classList.add(classname); |
|
| 19 |
+ } |
|
| 20 |
+ else |
|
| 21 |
+ {
|
|
| 22 |
+ // do nothing |
|
| 23 |
+ } |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ |
|
| 27 |
+ /** |
|
| 28 |
+ * sets up the control |
|
| 29 |
+ */ |
|
| 30 |
+ export function setup |
|
| 31 |
+ ( |
|
| 32 |
+ conf: type_conf, |
|
| 33 |
+ model: type_model |
|
| 34 |
+ ): void |
|
| 35 |
+ {
|
|
| 36 |
+ // connect |
|
| 37 |
+ {
|
|
| 38 |
+ register_listener |
|
| 39 |
+ ( |
|
| 40 |
+ document.querySelector<HTMLFormElement>("#connect > form"),
|
|
| 41 |
+ "submit", |
|
| 42 |
+ (e) => |
|
| 43 |
+ {
|
|
| 44 |
+ e.preventDefault(); |
|
| 45 |
+ const nickname: string = document.querySelector<HTMLInputElement>("#nickname").value;
|
|
| 46 |
+ const channel_names: Array<string> = document.querySelector<HTMLInputElement>("#channel").value.split(",");
|
|
| 47 |
+ ns_model.connect(conf, model, nickname, channel_names); |
|
| 48 |
+ } |
|
| 49 |
+ ); |
|
| 50 |
+ } |
|
| 51 |
+ // disconnect |
|
| 52 |
+ {
|
|
| 53 |
+ register_listener |
|
| 54 |
+ ( |
|
| 55 |
+ document.querySelector<HTMLButtonElement>("#disconnect"),
|
|
| 56 |
+ "click", |
|
| 57 |
+ (e) => |
|
| 58 |
+ {
|
|
| 59 |
+ ns_model.disconnect(conf, model); |
|
| 60 |
+ } |
|
| 61 |
+ ); |
|
| 62 |
+ } |
|
| 63 |
+ // send |
|
| 64 |
+ {
|
|
| 65 |
+ register_listener |
|
| 66 |
+ ( |
|
| 67 |
+ document.querySelector("#main > form"),
|
|
| 68 |
+ "submit", |
|
| 69 |
+ (e) => |
|
| 70 |
+ {
|
|
| 71 |
+ e.preventDefault(); |
|
| 72 |
+ const content: string = document.querySelector<HTMLInputElement>("#content").value;
|
|
| 73 |
+ ns_model.send(conf, model, content); |
|
| 74 |
+ } |
|
| 75 |
+ ); |
|
| 76 |
+ } |
|
| 77 |
+ // switch spot |
|
| 78 |
+ {
|
|
| 79 |
+ document.querySelectorAll(".spot").forEach
|
|
| 80 |
+ ( |
|
| 81 |
+ (dom_spot) => |
|
| 82 |
+ {
|
|
| 83 |
+ register_listener |
|
| 84 |
+ ( |
|
| 85 |
+ (dom_spot as HTMLElement), |
|
| 86 |
+ "click", |
|
| 87 |
+ (e) => |
|
| 88 |
+ {
|
|
| 89 |
+ const spot: type_spot = JSON.parse(dom_spot.getAttribute("rel"));
|
|
| 90 |
+ ns_model.set_active(model, spot); |
|
| 91 |
+ } |
|
| 92 |
+ ); |
|
| 93 |
+ } |
|
| 94 |
+ ); |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+} |
| ... | ... |
@@ -0,0 +1,65 @@ |
| 1 |
+/** |
|
| 2 |
+ * gets the current UNIX timestamp |
|
| 3 |
+ */ |
|
| 4 |
+function get_timestamp |
|
| 5 |
+( |
|
| 6 |
+): int |
|
| 7 |
+{
|
|
| 8 |
+ return Math.floor(Date.now()/1000); |
|
| 9 |
+} |
|
| 10 |
+ |
|
| 11 |
+ |
|
| 12 |
+/** |
|
| 13 |
+ * computes a floating point number in the interval [0,1[ out of a string |
|
| 14 |
+ */ |
|
| 15 |
+function hash_string_to_unit |
|
| 16 |
+( |
|
| 17 |
+ x: string |
|
| 18 |
+): float |
|
| 19 |
+{
|
|
| 20 |
+ return (x.split("").reduce((x, y) => ((x + y.charCodeAt(0)) % 32), 0) / 32);
|
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+ |
|
| 24 |
+/** |
|
| 25 |
+ * encodes a username as a CSS color |
|
| 26 |
+ */ |
|
| 27 |
+function get_usercolor |
|
| 28 |
+( |
|
| 29 |
+ name: string |
|
| 30 |
+): string |
|
| 31 |
+{
|
|
| 32 |
+ const hue: float = hash_string_to_unit(name); |
|
| 33 |
+ return `hsl(${(hue*360).toFixed(2)},50%,75%)`;
|
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+ |
|
| 37 |
+/** |
|
| 38 |
+ * calls an API action of the backend |
|
| 39 |
+ */ |
|
| 40 |
+async function backend_call |
|
| 41 |
+( |
|
| 42 |
+ conf: type_conf, |
|
| 43 |
+ connection_id: (null | string), |
|
| 44 |
+ action: string, data: any |
|
| 45 |
+): Promise<any> |
|
| 46 |
+{
|
|
| 47 |
+ const response: any = await fetch |
|
| 48 |
+ ( |
|
| 49 |
+ `${conf.backend.scheme}://${conf.backend.host}:${conf.backend.port.toFixed(0)}/${conf.backend.path}`,
|
|
| 50 |
+ {
|
|
| 51 |
+ "method": "POST", |
|
| 52 |
+ "body": JSON.stringify({"id": connection_id, "action": action, "data": data}),
|
|
| 53 |
+ } |
|
| 54 |
+ ); |
|
| 55 |
+ if (response.ok) |
|
| 56 |
+ {
|
|
| 57 |
+ return response.json(); |
|
| 58 |
+ } |
|
| 59 |
+ else |
|
| 60 |
+ {
|
|
| 61 |
+ console.error(response.text()); |
|
| 62 |
+ return Promise.reject<any>(new Error("backend call failed"));
|
|
| 63 |
+ } |
|
| 64 |
+} |
|
| 65 |
+ |
| ... | ... |
@@ -5,7 +5,6 @@ |
| 5 | 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | 6 |
<link rel="stylesheet" type="text/css" href="style.css"/> |
| 7 | 7 |
<script type="text/javascript" src="logic.js"></script> |
| 8 |
- <script type="text/javascript">init();</script> |
|
| 9 | 8 |
<title>wirc</title> |
| 10 | 9 |
</head> |
| 11 | 10 |
<body class="offline"> |
| ... | ... |
@@ -1,539 +0,0 @@ |
| 1 |
-type int = number; |
|
| 2 |
-type float = number; |
|
| 3 |
- |
|
| 4 |
- |
|
| 5 |
-var _conf: any = null; |
|
| 6 |
- |
|
| 7 |
- |
|
| 8 |
-function get_timestamp(): int |
|
| 9 |
-{
|
|
| 10 |
- return Math.floor(Date.now()/1000); |
|
| 11 |
-} |
|
| 12 |
- |
|
| 13 |
- |
|
| 14 |
-function hash_string_to_unit(x: string): float |
|
| 15 |
-{
|
|
| 16 |
- return (x.split("").reduce((x, y) => ((x + y.charCodeAt(0)) % 32), 0) / 32);
|
|
| 17 |
-} |
|
| 18 |
- |
|
| 19 |
- |
|
| 20 |
-function get_usercolor(name: string): string |
|
| 21 |
-{
|
|
| 22 |
- const hue: float = hash_string_to_unit(name); |
|
| 23 |
- return `hsl(${(hue*360).toFixed(2)},50%,75%)`;
|
|
| 24 |
-} |
|
| 25 |
- |
|
| 26 |
- |
|
| 27 |
-async function backend_call(connection_id: (null | string), action: string, data: any): Promise<any> |
|
| 28 |
-{
|
|
| 29 |
- const response = await fetch |
|
| 30 |
- ( |
|
| 31 |
- `${_conf["backend"]["scheme"]}://${_conf["backend"]["host"]}:${_conf["backend"]["port"].toFixed(0)}/${_conf["backend"]["path"]}`,
|
|
| 32 |
- {
|
|
| 33 |
- "method": "POST", |
|
| 34 |
- "body": JSON.stringify({"id": connection_id, "action": action, "data": data}),
|
|
| 35 |
- } |
|
| 36 |
- ); |
|
| 37 |
- if (response.ok) |
|
| 38 |
- {
|
|
| 39 |
- return response.json(); |
|
| 40 |
- } |
|
| 41 |
- else |
|
| 42 |
- {
|
|
| 43 |
- console.error(response.text()); |
|
| 44 |
- return Promise.reject<any>(new Error("backend call failed"));
|
|
| 45 |
- } |
|
| 46 |
-} |
|
| 47 |
- |
|
| 48 |
-enum enum_state |
|
| 49 |
-{
|
|
| 50 |
- offline = "offline", |
|
| 51 |
- connecting = "connecting", |
|
| 52 |
- online = "online", |
|
| 53 |
-} |
|
| 54 |
- |
|
| 55 |
- |
|
| 56 |
-type type_event = |
|
| 57 |
-{
|
|
| 58 |
- timestamp: int; |
|
| 59 |
- kind: string; |
|
| 60 |
- data: any; |
|
| 61 |
-}; |
|
| 62 |
- |
|
| 63 |
- |
|
| 64 |
-type type_spot = |
|
| 65 |
-{
|
|
| 66 |
- kind: string; |
|
| 67 |
- name: string; |
|
| 68 |
-}; |
|
| 69 |
- |
|
| 70 |
- |
|
| 71 |
-type type_entry = |
|
| 72 |
-{
|
|
| 73 |
- timestamp: int; |
|
| 74 |
- sender: string; |
|
| 75 |
- content: string; |
|
| 76 |
-}; |
|
| 77 |
- |
|
| 78 |
- |
|
| 79 |
-type type_user = |
|
| 80 |
-{
|
|
| 81 |
- name: string; |
|
| 82 |
- role: string; |
|
| 83 |
-}; |
|
| 84 |
- |
|
| 85 |
- |
|
| 86 |
-type type_channel = |
|
| 87 |
-{
|
|
| 88 |
- users: Array<type_user>; |
|
| 89 |
- entries: Array<type_entry>; |
|
| 90 |
-}; |
|
| 91 |
- |
|
| 92 |
- |
|
| 93 |
-type type_query = |
|
| 94 |
-{
|
|
| 95 |
- entries: Array<type_entry>; |
|
| 96 |
-}; |
|
| 97 |
- |
|
| 98 |
- |
|
| 99 |
-type type_model = |
|
| 100 |
-{
|
|
| 101 |
- state: enum_state; |
|
| 102 |
- connection_id: (null | string); |
|
| 103 |
- nickname: (null | string); |
|
| 104 |
- channels: Record<string, type_channel>; |
|
| 105 |
- queries: Record<string, type_query>; |
|
| 106 |
- active: (null | type_spot); |
|
| 107 |
-}; |
|
| 108 |
- |
|
| 109 |
- |
|
| 110 |
- |
|
| 111 |
-function model_set_state(model: type_model, state: enum_state): void |
|
| 112 |
-{
|
|
| 113 |
- model.state = state; |
|
| 114 |
- view_update_state(model); |
|
| 115 |
-} |
|
| 116 |
- |
|
| 117 |
- |
|
| 118 |
-function model_set_active(model: type_model, spot: type_spot): void |
|
| 119 |
-{
|
|
| 120 |
- model.active = spot; |
|
| 121 |
- view_update_spots(model); |
|
| 122 |
- view_update_entries(model); |
|
| 123 |
- view_update_users(model); |
|
| 124 |
-} |
|
| 125 |
- |
|
| 126 |
- |
|
| 127 |
-function model_process_events(model: type_model, events: Array<type_event>): void |
|
| 128 |
-{
|
|
| 129 |
- let shall_update_spots: boolean = false; |
|
| 130 |
- let shall_update_entries: boolean = false; |
|
| 131 |
- let shall_update_users: boolean = false; |
|
| 132 |
- for (const event of events) |
|
| 133 |
- {
|
|
| 134 |
- switch (event.kind) |
|
| 135 |
- {
|
|
| 136 |
- default: |
|
| 137 |
- {
|
|
| 138 |
- console.warn("unhandled event kind: " + event.kind);
|
|
| 139 |
- break; |
|
| 140 |
- } |
|
| 141 |
- case "userlist": |
|
| 142 |
- {
|
|
| 143 |
- model.channels[event.data["channel"]].users = event.data["users"]; |
|
| 144 |
- shall_update_users = true; |
|
| 145 |
- break; |
|
| 146 |
- } |
|
| 147 |
- case "message_channel": |
|
| 148 |
- {
|
|
| 149 |
- model.channels[event.data["channel"]].entries.push |
|
| 150 |
- ({
|
|
| 151 |
- "timestamp": event.timestamp, |
|
| 152 |
- "sender": event.data["sender"], |
|
| 153 |
- "content": event.data["content"], |
|
| 154 |
- }); |
|
| 155 |
- shall_update_entries = true; |
|
| 156 |
- break; |
|
| 157 |
- } |
|
| 158 |
- case "message_query": |
|
| 159 |
- {
|
|
| 160 |
- if (! model.queries.hasOwnProperty(event.data["user_name"])) |
|
| 161 |
- {
|
|
| 162 |
- model.queries[event.data["user_name"]] = {"entries": []};
|
|
| 163 |
- shall_update_spots = true; |
|
| 164 |
- } |
|
| 165 |
- else |
|
| 166 |
- {
|
|
| 167 |
- // do nothing |
|
| 168 |
- } |
|
| 169 |
- model.queries[event.data["user_name"]].entries.push |
|
| 170 |
- ({
|
|
| 171 |
- "timestamp": event.timestamp, |
|
| 172 |
- "sender": event.data["sender"], |
|
| 173 |
- "content": event.data["content"], |
|
| 174 |
- }); |
|
| 175 |
- shall_update_entries = true; |
|
| 176 |
- break; |
|
| 177 |
- } |
|
| 178 |
- } |
|
| 179 |
- } |
|
| 180 |
- |
|
| 181 |
- // update view |
|
| 182 |
- {
|
|
| 183 |
- if (shall_update_spots) view_update_spots(model); |
|
| 184 |
- if (shall_update_entries) view_update_entries(model); |
|
| 185 |
- if (shall_update_users) view_update_users(model); |
|
| 186 |
- } |
|
| 187 |
-} |
|
| 188 |
- |
|
| 189 |
- |
|
| 190 |
- |
|
| 191 |
-function view_update_state(model: type_model): void |
|
| 192 |
-{
|
|
| 193 |
- document.querySelector("body").setAttribute("class", model.state);
|
|
| 194 |
-} |
|
| 195 |
- |
|
| 196 |
- |
|
| 197 |
-function view_update_spots(model: type_model): void |
|
| 198 |
-{
|
|
| 199 |
- let dom_spots: HTMLUListElement = document.querySelector("#spots");
|
|
| 200 |
- const spots: Array<type_spot> = ( |
|
| 201 |
- [] |
|
| 202 |
- .concat(Object.keys(model.channels).map((name) => ({"kind": "channel", "name": name})))
|
|
| 203 |
- .concat(Object.keys(model.queries).map((name) => ({"kind": "query", "name": name})))
|
|
| 204 |
- ); |
|
| 205 |
- dom_spots.textContent = ""; |
|
| 206 |
- for (const spot of spots) |
|
| 207 |
- {
|
|
| 208 |
- let dom_spot: HTMLLIElement = document.createElement("li");
|
|
| 209 |
- dom_spot.classList.add("spot");
|
|
| 210 |
- {
|
|
| 211 |
- let dom_kind: HTMLSpanElement = document.createElement("span");
|
|
| 212 |
- dom_kind.classList.add("spot_kind");
|
|
| 213 |
- dom_kind.textContent = spot.kind; |
|
| 214 |
- dom_spot.appendChild(dom_kind); |
|
| 215 |
- } |
|
| 216 |
- {
|
|
| 217 |
- let dom_name: HTMLSpanElement = document.createElement("span");
|
|
| 218 |
- dom_name.classList.add("spot_sender");
|
|
| 219 |
- dom_name.textContent = spot.name; |
|
| 220 |
- dom_spot.appendChild(dom_name); |
|
| 221 |
- } |
|
| 222 |
- dom_spot.classList.toggle("spot_active", ((spot.kind === model.active.kind) && (spot.name === model.active.name)));
|
|
| 223 |
- dom_spot.addEventListener |
|
| 224 |
- ( |
|
| 225 |
- "click", |
|
| 226 |
- (e) => |
|
| 227 |
- {
|
|
| 228 |
- model_set_active(model, spot); |
|
| 229 |
- } |
|
| 230 |
- ); |
|
| 231 |
- dom_spots.appendChild(dom_spot); |
|
| 232 |
- } |
|
| 233 |
-} |
|
| 234 |
- |
|
| 235 |
- |
|
| 236 |
-function view_update_entries(model: type_model): void |
|
| 237 |
-{
|
|
| 238 |
- let dom_entries: HTMLUListElement = document.querySelector("#entries");
|
|
| 239 |
- let entries: Array<type_entry>; |
|
| 240 |
- switch (model.active.kind) |
|
| 241 |
- {
|
|
| 242 |
- case "channel": |
|
| 243 |
- {
|
|
| 244 |
- entries = model.channels[model.active.name].entries; |
|
| 245 |
- break; |
|
| 246 |
- } |
|
| 247 |
- case "query": |
|
| 248 |
- {
|
|
| 249 |
- entries = model.queries[model.active.name].entries; |
|
| 250 |
- break; |
|
| 251 |
- } |
|
| 252 |
- } |
|
| 253 |
- dom_entries.textContent = ""; |
|
| 254 |
- for (const entry of entries) |
|
| 255 |
- {
|
|
| 256 |
- let dom_entry: HTMLLIElement = document.createElement("li");
|
|
| 257 |
- dom_entry.classList.add("entry");
|
|
| 258 |
- {
|
|
| 259 |
- let dom_time: HTMLSpanElement = document.createElement("span");
|
|
| 260 |
- dom_time.classList.add("entry_time");
|
|
| 261 |
- dom_time.textContent = (new Date(entry.timestamp*1000)).toISOString().slice(11, 19); |
|
| 262 |
- dom_entry.appendChild(dom_time); |
|
| 263 |
- } |
|
| 264 |
- {
|
|
| 265 |
- let dom_sender: HTMLSpanElement = document.createElement("span");
|
|
| 266 |
- dom_sender.classList.add("entry_sender");
|
|
| 267 |
- dom_sender.style.color = get_usercolor(entry.sender); |
|
| 268 |
- dom_sender.textContent = entry.sender; |
|
| 269 |
- dom_entry.appendChild(dom_sender); |
|
| 270 |
- } |
|
| 271 |
- {
|
|
| 272 |
- let dom_content: HTMLSpanElement = document.createElement("span");
|
|
| 273 |
- dom_content.classList.add("entry_content");
|
|
| 274 |
- dom_content.textContent = entry.content; |
|
| 275 |
- dom_entry.appendChild(dom_content); |
|
| 276 |
- } |
|
| 277 |
- dom_entries.appendChild(dom_entry); |
|
| 278 |
- } |
|
| 279 |
- dom_entries.scrollTo(0, dom_entries["scrollTopMax"]); |
|
| 280 |
-} |
|
| 281 |
- |
|
| 282 |
- |
|
| 283 |
-function view_update_users(model: type_model): void |
|
| 284 |
-{
|
|
| 285 |
- let dom_users: HTMLUListElement = document.querySelector("#users");
|
|
| 286 |
- dom_users.textContent = ""; |
|
| 287 |
- let users: Array<type_user>; |
|
| 288 |
- switch (model.active.kind) |
|
| 289 |
- {
|
|
| 290 |
- default: |
|
| 291 |
- {
|
|
| 292 |
- console.warn("unhandled kind: " + model.active.kind);
|
|
| 293 |
- users = []; |
|
| 294 |
- break; |
|
| 295 |
- } |
|
| 296 |
- case "channel": |
|
| 297 |
- {
|
|
| 298 |
- users = model.channels[model.active.name].users; |
|
| 299 |
- break; |
|
| 300 |
- } |
|
| 301 |
- case "query": |
|
| 302 |
- {
|
|
| 303 |
- users = [{"name": model.nickname, "role": ""}, {"name": model.active.name, "role": ""}];
|
|
| 304 |
- break; |
|
| 305 |
- } |
|
| 306 |
- } |
|
| 307 |
- const users_sorted: Array<type_user> = users.sort |
|
| 308 |
- ( |
|
| 309 |
- (x, y) => |
|
| 310 |
- ( |
|
| 311 |
- (x.role >= y.role) |
|
| 312 |
- ? -1 |
|
| 313 |
- : ( |
|
| 314 |
- (x.role === y.role) |
|
| 315 |
- ? ((x.name < y.name) ? -1 : +1) |
|
| 316 |
- : +1 |
|
| 317 |
- ) |
|
| 318 |
- ) |
|
| 319 |
- ); |
|
| 320 |
- for (const user of users_sorted) |
|
| 321 |
- {
|
|
| 322 |
- let dom_user: HTMLLIElement = document.createElement("li");
|
|
| 323 |
- dom_user.classList.add("user");
|
|
| 324 |
- {
|
|
| 325 |
- let dom_role: HTMLSpanElement = document.createElement("span");
|
|
| 326 |
- dom_role.textContent = user.role; |
|
| 327 |
- dom_user.appendChild(dom_role); |
|
| 328 |
- } |
|
| 329 |
- {
|
|
| 330 |
- let dom_name: HTMLSpanElement = document.createElement("span");
|
|
| 331 |
- dom_name.textContent = user.name; |
|
| 332 |
- dom_name.style.color = get_usercolor(user.name); |
|
| 333 |
- dom_user.appendChild(dom_name); |
|
| 334 |
- } |
|
| 335 |
- dom_users.appendChild(dom_user); |
|
| 336 |
- } |
|
| 337 |
-} |
|
| 338 |
- |
|
| 339 |
- |
|
| 340 |
-function view_setup(model: type_model): void |
|
| 341 |
-{
|
|
| 342 |
- document.querySelector<HTMLInputElement>("#channel").value = _conf["irc"]["predefined_channel"];
|
|
| 343 |
- document.querySelector<HTMLInputElement>("#nickname").value = (_conf["irc"]["predefined_nickname_prefix"] + (Math.random()*100).toFixed(0));
|
|
| 344 |
- setInterval |
|
| 345 |
- ( |
|
| 346 |
- async () => |
|
| 347 |
- {
|
|
| 348 |
- switch (model.state) |
|
| 349 |
- {
|
|
| 350 |
- default: |
|
| 351 |
- {
|
|
| 352 |
- throw (new Error("invalid state: " + model.state));
|
|
| 353 |
- break; |
|
| 354 |
- } |
|
| 355 |
- case enum_state.offline: |
|
| 356 |
- {
|
|
| 357 |
- // do nothing |
|
| 358 |
- break; |
|
| 359 |
- } |
|
| 360 |
- case enum_state.connecting: |
|
| 361 |
- {
|
|
| 362 |
- const ready: boolean = await backend_call(model.connection_id, "check", null); |
|
| 363 |
- if (ready) |
|
| 364 |
- {
|
|
| 365 |
- model_set_state(model, enum_state.online); |
|
| 366 |
- } |
|
| 367 |
- else |
|
| 368 |
- {
|
|
| 369 |
- // do nothing |
|
| 370 |
- } |
|
| 371 |
- break; |
|
| 372 |
- } |
|
| 373 |
- case enum_state.online: |
|
| 374 |
- {
|
|
| 375 |
- const events: Array<type_event> = await backend_call(model.connection_id, "fetch", null); |
|
| 376 |
- model_process_events(model, events); |
|
| 377 |
- break; |
|
| 378 |
- } |
|
| 379 |
- } |
|
| 380 |
- }, |
|
| 381 |
- _conf["settings"]["poll_interval_in_milliseconds"] |
|
| 382 |
- ); |
|
| 383 |
- model_set_state(model, enum_state.offline); |
|
| 384 |
-} |
|
| 385 |
- |
|
| 386 |
- |
|
| 387 |
-function control_setup(model: type_model): void |
|
| 388 |
-{
|
|
| 389 |
- document.querySelector("#connect > form").addEventListener
|
|
| 390 |
- ( |
|
| 391 |
- "submit", |
|
| 392 |
- async (event) => |
|
| 393 |
- {
|
|
| 394 |
- event.preventDefault(); |
|
| 395 |
- |
|
| 396 |
- model_set_state(model, enum_state.connecting); |
|
| 397 |
- |
|
| 398 |
- let dom_nickname: HTMLInputElement = document.querySelector<HTMLInputElement>("#nickname");
|
|
| 399 |
- let dom_channel: HTMLInputElement = document.querySelector<HTMLInputElement>("#channel");
|
|
| 400 |
- const nickname: string = dom_nickname.value; |
|
| 401 |
- const channel_names: Array<string> = dom_channel.value.split(",");
|
|
| 402 |
- |
|
| 403 |
- const connection_id: string = await backend_call |
|
| 404 |
- ( |
|
| 405 |
- model.connection_id, |
|
| 406 |
- "connect", |
|
| 407 |
- {
|
|
| 408 |
- "server": _conf["irc"]["server"], |
|
| 409 |
- "channels": channel_names, |
|
| 410 |
- "nickname": nickname, |
|
| 411 |
- } |
|
| 412 |
- ); |
|
| 413 |
- model.connection_id = connection_id; |
|
| 414 |
- model.nickname = nickname; |
|
| 415 |
- for (const channel_name of channel_names) |
|
| 416 |
- {
|
|
| 417 |
- model.channels[channel_name] = |
|
| 418 |
- {
|
|
| 419 |
- "users": [], |
|
| 420 |
- "entries": [], |
|
| 421 |
- }; |
|
| 422 |
- } |
|
| 423 |
- // TODO: can crash |
|
| 424 |
- model_set_active(model, {"kind": "channel", "name": channel_names[0]});
|
|
| 425 |
- } |
|
| 426 |
- ); |
|
| 427 |
- document.querySelector("#disconnect").addEventListener
|
|
| 428 |
- ( |
|
| 429 |
- "click", |
|
| 430 |
- async (event) => |
|
| 431 |
- {
|
|
| 432 |
- await backend_call |
|
| 433 |
- ( |
|
| 434 |
- model.connection_id, |
|
| 435 |
- "disconnect", |
|
| 436 |
- null |
|
| 437 |
- ); |
|
| 438 |
- model_set_state(model, enum_state.offline); |
|
| 439 |
- model.connection_id = null; |
|
| 440 |
- } |
|
| 441 |
- ); |
|
| 442 |
- document.querySelector("#main > form").addEventListener
|
|
| 443 |
- ( |
|
| 444 |
- "submit", |
|
| 445 |
- async (e) => |
|
| 446 |
- {
|
|
| 447 |
- event.preventDefault(); |
|
| 448 |
- |
|
| 449 |
- let dom_content: HTMLInputElement = document.querySelector<HTMLInputElement>("#content");
|
|
| 450 |
- const content: string = dom_content.value; |
|
| 451 |
- dom_content.value = ""; |
|
| 452 |
- dom_content.focus(); |
|
| 453 |
- switch (model.active.kind) |
|
| 454 |
- {
|
|
| 455 |
- case "channel": |
|
| 456 |
- {
|
|
| 457 |
- const event: type_event = |
|
| 458 |
- {
|
|
| 459 |
- "timestamp": get_timestamp(), |
|
| 460 |
- "kind": "message_channel", |
|
| 461 |
- "data": |
|
| 462 |
- {
|
|
| 463 |
- "channel": model.active.name, |
|
| 464 |
- "sender": model.nickname, |
|
| 465 |
- "content": content, |
|
| 466 |
- } |
|
| 467 |
- }; |
|
| 468 |
- model_process_events(model, [event]); |
|
| 469 |
- view_update_entries(model); |
|
| 470 |
- backend_call |
|
| 471 |
- ( |
|
| 472 |
- model.connection_id, |
|
| 473 |
- "send_channel", |
|
| 474 |
- {
|
|
| 475 |
- "channel": model.active.name, |
|
| 476 |
- "content": content, |
|
| 477 |
- } |
|
| 478 |
- ); |
|
| 479 |
- break; |
|
| 480 |
- } |
|
| 481 |
- case "query": |
|
| 482 |
- {
|
|
| 483 |
- const event: type_event = |
|
| 484 |
- {
|
|
| 485 |
- "timestamp": get_timestamp(), |
|
| 486 |
- "kind": "message_query", |
|
| 487 |
- "data": |
|
| 488 |
- {
|
|
| 489 |
- "user_name": model.active.name, |
|
| 490 |
- "sender": model.nickname, |
|
| 491 |
- "content": content, |
|
| 492 |
- } |
|
| 493 |
- }; |
|
| 494 |
- model_process_events(model, [event]); |
|
| 495 |
- backend_call |
|
| 496 |
- ( |
|
| 497 |
- model.connection_id, |
|
| 498 |
- "send_query", |
|
| 499 |
- {
|
|
| 500 |
- "receiver": model.active.name, |
|
| 501 |
- "content": content, |
|
| 502 |
- } |
|
| 503 |
- ); |
|
| 504 |
- break; |
|
| 505 |
- } |
|
| 506 |
- } |
|
| 507 |
- } |
|
| 508 |
- ); |
|
| 509 |
-} |
|
| 510 |
- |
|
| 511 |
- |
|
| 512 |
-var _model; |
|
| 513 |
-async function main(): Promise<void> |
|
| 514 |
-{
|
|
| 515 |
- _conf = await fetch("conf.json").then(x => x.json());
|
|
| 516 |
- const model: type_model = |
|
| 517 |
- {
|
|
| 518 |
- "state": enum_state.offline, |
|
| 519 |
- "connection_id": null, |
|
| 520 |
- "nickname": null, |
|
| 521 |
- "channels": {},
|
|
| 522 |
- "queries": {},
|
|
| 523 |
- "active": null, |
|
| 524 |
- }; |
|
| 525 |
-_model = model; |
|
| 526 |
- view_setup(model); |
|
| 527 |
- control_setup(model); |
|
| 528 |
-} |
|
| 529 |
- |
|
| 530 |
- |
|
| 531 |
-function init(): void |
|
| 532 |
-{
|
|
| 533 |
- document.addEventListener |
|
| 534 |
- ( |
|
| 535 |
- "DOMContentLoaded", |
|
| 536 |
- (event) => {main();}
|
|
| 537 |
- ); |
|
| 538 |
-} |
|
| 539 |
- |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 1 |
+var _model; |
|
| 2 |
+/** |
|
| 3 |
+ * initializes the system |
|
| 4 |
+ */ |
|
| 5 |
+async function main |
|
| 6 |
+( |
|
| 7 |
+): Promise<void> |
|
| 8 |
+{
|
|
| 9 |
+ const conf: type_conf = await fetch("conf.json").then<type_conf>(x => x.json());
|
|
| 10 |
+ const model: type_model = |
|
| 11 |
+ {
|
|
| 12 |
+ "state": enum_state.offline, |
|
| 13 |
+ "connection_id": null, |
|
| 14 |
+ "nickname": null, |
|
| 15 |
+ "channels": {},
|
|
| 16 |
+ "queries": {},
|
|
| 17 |
+ "active": null, |
|
| 18 |
+ "listeners": {},
|
|
| 19 |
+ }; |
|
| 20 |
+_model = model; |
|
| 21 |
+ ns_model.setup(conf, model); |
|
| 22 |
+ ns_view.setup(conf, model); |
|
| 23 |
+ ns_control.setup(conf, model); |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+ |
|
| 27 |
+document.addEventListener("DOMContentLoaded", () => {main();});
|
|
| 28 |
+ |
| ... | ... |
@@ -0,0 +1,331 @@ |
| 1 |
+namespace ns_model |
|
| 2 |
+{
|
|
| 3 |
+ |
|
| 4 |
+ /** |
|
| 5 |
+ * adds a listener for a certain incident |
|
| 6 |
+ */ |
|
| 7 |
+ export function listen |
|
| 8 |
+ ( |
|
| 9 |
+ model: type_model, |
|
| 10 |
+ incident: string, |
|
| 11 |
+ handler: (details?: any)=>void |
|
| 12 |
+ ): void |
|
| 13 |
+ {
|
|
| 14 |
+ if (! model.listeners.hasOwnProperty(incident)) |
|
| 15 |
+ {
|
|
| 16 |
+ model.listeners[incident] = []; |
|
| 17 |
+ } |
|
| 18 |
+ else |
|
| 19 |
+ {
|
|
| 20 |
+ // do nothing |
|
| 21 |
+ } |
|
| 22 |
+ model.listeners[incident].push(handler); |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ |
|
| 26 |
+ /** |
|
| 27 |
+ * sends a notification to all listeners for a certain incident |
|
| 28 |
+ */ |
|
| 29 |
+ function notify |
|
| 30 |
+ ( |
|
| 31 |
+ model: type_model, |
|
| 32 |
+ incident: string, |
|
| 33 |
+ details: any = null |
|
| 34 |
+ ): void |
|
| 35 |
+ {
|
|
| 36 |
+ if (model.listeners.hasOwnProperty(incident)) |
|
| 37 |
+ {
|
|
| 38 |
+ for (const handler of model.listeners[incident]) |
|
| 39 |
+ {
|
|
| 40 |
+ handler(details); |
|
| 41 |
+ } |
|
| 42 |
+ } |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ |
|
| 46 |
+ /** |
|
| 47 |
+ * sets the state |
|
| 48 |
+ */ |
|
| 49 |
+ export function set_state |
|
| 50 |
+ ( |
|
| 51 |
+ model: type_model, |
|
| 52 |
+ state: enum_state |
|
| 53 |
+ ): void |
|
| 54 |
+ {
|
|
| 55 |
+ model.state = state; |
|
| 56 |
+ notify(model, "state_changed"); |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ |
|
| 60 |
+ /** |
|
| 61 |
+ * sets the active spot (channel or query) |
|
| 62 |
+ */ |
|
| 63 |
+ export function set_active |
|
| 64 |
+ ( |
|
| 65 |
+ model: type_model, |
|
| 66 |
+ spot: type_spot |
|
| 67 |
+ ): void |
|
| 68 |
+ {
|
|
| 69 |
+ model.active = spot; |
|
| 70 |
+ notify(model, "spots_changed"); |
|
| 71 |
+ notify(model, "entries_changed"); |
|
| 72 |
+ notify(model, "users_changed"); |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ |
|
| 76 |
+ /** |
|
| 77 |
+ * updates the model according to a list of events |
|
| 78 |
+ */ |
|
| 79 |
+ function process_events |
|
| 80 |
+ ( |
|
| 81 |
+ model: type_model, |
|
| 82 |
+ events: Array<type_event> |
|
| 83 |
+ ): void |
|
| 84 |
+ {
|
|
| 85 |
+ let shall_update_spots: boolean = false; |
|
| 86 |
+ let shall_update_entries: boolean = false; |
|
| 87 |
+ let shall_update_users: boolean = false; |
|
| 88 |
+ |
|
| 89 |
+ for (const event of events) |
|
| 90 |
+ {
|
|
| 91 |
+ switch (event.kind) |
|
| 92 |
+ {
|
|
| 93 |
+ default: |
|
| 94 |
+ {
|
|
| 95 |
+ console.warn("unhandled event kind: " + event.kind);
|
|
| 96 |
+ break; |
|
| 97 |
+ } |
|
| 98 |
+ case "userlist": |
|
| 99 |
+ {
|
|
| 100 |
+ model.channels[event.data["channel"]].users = event.data["users"]; |
|
| 101 |
+ shall_update_users = true; |
|
| 102 |
+ break; |
|
| 103 |
+ } |
|
| 104 |
+ case "message_channel": |
|
| 105 |
+ {
|
|
| 106 |
+ model.channels[event.data["channel"]].entries.push |
|
| 107 |
+ ({
|
|
| 108 |
+ "timestamp": event.timestamp, |
|
| 109 |
+ "sender": event.data["sender"], |
|
| 110 |
+ "content": event.data["content"], |
|
| 111 |
+ }); |
|
| 112 |
+ shall_update_entries = true; |
|
| 113 |
+ break; |
|
| 114 |
+ } |
|
| 115 |
+ case "message_query": |
|
| 116 |
+ {
|
|
| 117 |
+ if (! model.queries.hasOwnProperty(event.data["user_name"])) |
|
| 118 |
+ {
|
|
| 119 |
+ model.queries[event.data["user_name"]] = {"entries": []};
|
|
| 120 |
+ shall_update_spots = true; |
|
| 121 |
+ } |
|
| 122 |
+ else |
|
| 123 |
+ {
|
|
| 124 |
+ // do nothing |
|
| 125 |
+ } |
|
| 126 |
+ model.queries[event.data["user_name"]].entries.push |
|
| 127 |
+ ({
|
|
| 128 |
+ "timestamp": event.timestamp, |
|
| 129 |
+ "sender": event.data["sender"], |
|
| 130 |
+ "content": event.data["content"], |
|
| 131 |
+ }); |
|
| 132 |
+ shall_update_entries = true; |
|
| 133 |
+ break; |
|
| 134 |
+ } |
|
| 135 |
+ } |
|
| 136 |
+ } |
|
| 137 |
+ |
|
| 138 |
+ if (shall_update_spots) notify(model, "spots_changed"); |
|
| 139 |
+ if (shall_update_entries) notify(model, "entries_changed"); |
|
| 140 |
+ if (shall_update_users) notify(model, "users_changed"); |
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+ |
|
| 144 |
+ /** |
|
| 145 |
+ * establishes the connection |
|
| 146 |
+ */ |
|
| 147 |
+ export async function connect |
|
| 148 |
+ ( |
|
| 149 |
+ conf: type_conf, |
|
| 150 |
+ model: type_model, |
|
| 151 |
+ nickname: string, |
|
| 152 |
+ channel_names: Array<string> |
|
| 153 |
+ ): Promise<void> |
|
| 154 |
+ {
|
|
| 155 |
+ set_state(model, enum_state.connecting); |
|
| 156 |
+ const connection_id: string = await backend_call |
|
| 157 |
+ ( |
|
| 158 |
+ conf, |
|
| 159 |
+ model.connection_id, |
|
| 160 |
+ "connect", |
|
| 161 |
+ {
|
|
| 162 |
+ "server": conf.irc.server, |
|
| 163 |
+ "channels": channel_names, |
|
| 164 |
+ "nickname": nickname, |
|
| 165 |
+ } |
|
| 166 |
+ ); |
|
| 167 |
+ model.connection_id = connection_id; |
|
| 168 |
+ model.nickname = nickname; |
|
| 169 |
+ for (const channel_name of channel_names) |
|
| 170 |
+ {
|
|
| 171 |
+ model.channels[channel_name] = |
|
| 172 |
+ {
|
|
| 173 |
+ "users": [], |
|
| 174 |
+ "entries": [], |
|
| 175 |
+ }; |
|
| 176 |
+ } |
|
| 177 |
+ if (channel_names.length > 0) |
|
| 178 |
+ {
|
|
| 179 |
+ set_active(model, {"kind": "channel", "name": channel_names[0]});
|
|
| 180 |
+ } |
|
| 181 |
+ return Promise.resolve<void>(undefined); |
|
| 182 |
+ } |
|
| 183 |
+ |
|
| 184 |
+ |
|
| 185 |
+ /** |
|
| 186 |
+ * closes the connection |
|
| 187 |
+ */ |
|
| 188 |
+ export async function disconnect |
|
| 189 |
+ ( |
|
| 190 |
+ conf: type_conf, |
|
| 191 |
+ model: type_model |
|
| 192 |
+ ): Promise<void> |
|
| 193 |
+ {
|
|
| 194 |
+ await backend_call |
|
| 195 |
+ ( |
|
| 196 |
+ conf, |
|
| 197 |
+ model.connection_id, |
|
| 198 |
+ "disconnect", |
|
| 199 |
+ null |
|
| 200 |
+ ); |
|
| 201 |
+ set_state(model, enum_state.offline); |
|
| 202 |
+ model.connection_id = null; |
|
| 203 |
+ return Promise.resolve<void>(undefined); |
|
| 204 |
+ } |
|
| 205 |
+ |
|
| 206 |
+ |
|
| 207 |
+ /** |
|
| 208 |
+ * adds a client side message |
|
| 209 |
+ */ |
|
| 210 |
+ export function send |
|
| 211 |
+ ( |
|
| 212 |
+ conf: type_conf, |
|
| 213 |
+ model: type_model, |
|
| 214 |
+ content: string |
|
| 215 |
+ ): void |
|
| 216 |
+ {
|
|
| 217 |
+ switch (model.active.kind) |
|
| 218 |
+ {
|
|
| 219 |
+ case "channel": |
|
| 220 |
+ {
|
|
| 221 |
+ backend_call |
|
| 222 |
+ ( |
|
| 223 |
+ conf, |
|
| 224 |
+ model.connection_id, |
|
| 225 |
+ "send_channel", |
|
| 226 |
+ {
|
|
| 227 |
+ "channel": model.active.name, |
|
| 228 |
+ "content": content, |
|
| 229 |
+ } |
|
| 230 |
+ ); |
|
| 231 |
+ const event: type_event = |
|
| 232 |
+ {
|
|
| 233 |
+ "timestamp": get_timestamp(), |
|
| 234 |
+ "kind": "message_channel", |
|
| 235 |
+ "data": |
|
| 236 |
+ {
|
|
| 237 |
+ "channel": model.active.name, |
|
| 238 |
+ "sender": model.nickname, |
|
| 239 |
+ "content": content, |
|
| 240 |
+ } |
|
| 241 |
+ }; |
|
| 242 |
+ process_events(model, [event]); |
|
| 243 |
+ notify(model, "entries_changed"); |
|
| 244 |
+ notify(model, "message_sent"); |
|
| 245 |
+ break; |
|
| 246 |
+ } |
|
| 247 |
+ case "query": |
|
| 248 |
+ {
|
|
| 249 |
+ backend_call |
|
| 250 |
+ ( |
|
| 251 |
+ conf, |
|
| 252 |
+ model.connection_id, |
|
| 253 |
+ "send_query", |
|
| 254 |
+ {
|
|
| 255 |
+ "receiver": model.active.name, |
|
| 256 |
+ "content": content, |
|
| 257 |
+ } |
|
| 258 |
+ ); |
|
| 259 |
+ const event: type_event = |
|
| 260 |
+ {
|
|
| 261 |
+ "timestamp": get_timestamp(), |
|
| 262 |
+ "kind": "message_query", |
|
| 263 |
+ "data": |
|
| 264 |
+ {
|
|
| 265 |
+ "user_name": model.active.name, |
|
| 266 |
+ "sender": model.nickname, |
|
| 267 |
+ "content": content, |
|
| 268 |
+ } |
|
| 269 |
+ }; |
|
| 270 |
+ process_events(model, [event]); |
|
| 271 |
+ notify(model, "entries_changed"); |
|
| 272 |
+ notify(model, "message_sent"); |
|
| 273 |
+ break; |
|
| 274 |
+ } |
|
| 275 |
+ } |
|
| 276 |
+ } |
|
| 277 |
+ |
|
| 278 |
+ |
|
| 279 |
+ /** |
|
| 280 |
+ * sets up the model |
|
| 281 |
+ */ |
|
| 282 |
+ export function setup |
|
| 283 |
+ ( |
|
| 284 |
+ conf: type_conf, |
|
| 285 |
+ model: type_model |
|
| 286 |
+ ): void |
|
| 287 |
+ {
|
|
| 288 |
+ setInterval |
|
| 289 |
+ ( |
|
| 290 |
+ async () => |
|
| 291 |
+ {
|
|
| 292 |
+ switch (model.state) |
|
| 293 |
+ {
|
|
| 294 |
+ default: |
|
| 295 |
+ {
|
|
| 296 |
+ throw (new Error(`invalid state '${model.state}'`));
|
|
| 297 |
+ break; |
|
| 298 |
+ } |
|
| 299 |
+ case enum_state.offline: |
|
| 300 |
+ {
|
|
| 301 |
+ // do nothing |
|
| 302 |
+ break; |
|
| 303 |
+ } |
|
| 304 |
+ case enum_state.connecting: |
|
| 305 |
+ {
|
|
| 306 |
+ const ready: boolean = await backend_call(conf, model.connection_id, "check", null); |
|
| 307 |
+ if (ready) |
|
| 308 |
+ {
|
|
| 309 |
+ set_state(model, enum_state.online); |
|
| 310 |
+ } |
|
| 311 |
+ else |
|
| 312 |
+ {
|
|
| 313 |
+ // do nothing |
|
| 314 |
+ } |
|
| 315 |
+ break; |
|
| 316 |
+ } |
|
| 317 |
+ case enum_state.online: |
|
| 318 |
+ {
|
|
| 319 |
+ const events: Array<type_event> = await backend_call(conf, model.connection_id, "fetch", null); |
|
| 320 |
+ process_events(model, events); |
|
| 321 |
+ break; |
|
| 322 |
+ } |
|
| 323 |
+ } |
|
| 324 |
+ }, |
|
| 325 |
+ conf.settings.poll_interval_in_milliseconds |
|
| 326 |
+ ); |
|
| 327 |
+ set_state(model, enum_state.offline); |
|
| 328 |
+ } |
|
| 329 |
+ |
|
| 330 |
+} |
|
| 331 |
+ |
| ... | ... |
@@ -0,0 +1,91 @@ |
| 1 |
+type int = number; |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+type float = number; |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+type type_conf = |
|
| 8 |
+{
|
|
| 9 |
+ backend: |
|
| 10 |
+ {
|
|
| 11 |
+ scheme: string; |
|
| 12 |
+ host: string; |
|
| 13 |
+ port: int; |
|
| 14 |
+ path: string; |
|
| 15 |
+ }; |
|
| 16 |
+ settings: |
|
| 17 |
+ {
|
|
| 18 |
+ poll_interval_in_milliseconds: 2000; |
|
| 19 |
+ }; |
|
| 20 |
+ irc: |
|
| 21 |
+ {
|
|
| 22 |
+ server: string; |
|
| 23 |
+ predefined_channel: string; |
|
| 24 |
+ predefined_nickname_prefix: string; |
|
| 25 |
+ } |
|
| 26 |
+}; |
|
| 27 |
+ |
|
| 28 |
+ |
|
| 29 |
+enum enum_state |
|
| 30 |
+{
|
|
| 31 |
+ offline = "offline", |
|
| 32 |
+ connecting = "connecting", |
|
| 33 |
+ online = "online", |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+ |
|
| 37 |
+type type_event = |
|
| 38 |
+{
|
|
| 39 |
+ timestamp: int; |
|
| 40 |
+ kind: string; |
|
| 41 |
+ data: any; |
|
| 42 |
+}; |
|
| 43 |
+ |
|
| 44 |
+ |
|
| 45 |
+type type_spot = |
|
| 46 |
+{
|
|
| 47 |
+ kind: string; |
|
| 48 |
+ name: string; |
|
| 49 |
+}; |
|
| 50 |
+ |
|
| 51 |
+ |
|
| 52 |
+type type_entry = |
|
| 53 |
+{
|
|
| 54 |
+ timestamp: int; |
|
| 55 |
+ sender: string; |
|
| 56 |
+ content: string; |
|
| 57 |
+}; |
|
| 58 |
+ |
|
| 59 |
+ |
|
| 60 |
+type type_user = |
|
| 61 |
+{
|
|
| 62 |
+ name: string; |
|
| 63 |
+ role: string; |
|
| 64 |
+}; |
|
| 65 |
+ |
|
| 66 |
+ |
|
| 67 |
+type type_channel = |
|
| 68 |
+{
|
|
| 69 |
+ users: Array<type_user>; |
|
| 70 |
+ entries: Array<type_entry>; |
|
| 71 |
+}; |
|
| 72 |
+ |
|
| 73 |
+ |
|
| 74 |
+type type_query = |
|
| 75 |
+{
|
|
| 76 |
+ entries: Array<type_entry>; |
|
| 77 |
+}; |
|
| 78 |
+ |
|
| 79 |
+ |
|
| 80 |
+type type_model = |
|
| 81 |
+{
|
|
| 82 |
+ state: enum_state; |
|
| 83 |
+ connection_id: (null | string); |
|
| 84 |
+ nickname: (null | string); |
|
| 85 |
+ channels: Record<string, type_channel>; |
|
| 86 |
+ queries: Record<string, type_query>; |
|
| 87 |
+ active: (null | type_spot); |
|
| 88 |
+ listeners: Record<string, Array<(details?: any)=>void>>; |
|
| 89 |
+}; |
|
| 90 |
+ |
|
| 91 |
+ |
| ... | ... |
@@ -0,0 +1,205 @@ |
| 1 |
+namespace ns_view |
|
| 2 |
+{
|
|
| 3 |
+ |
|
| 4 |
+ /** |
|
| 5 |
+ * updates the state (switches between login, connecting and regular "page") |
|
| 6 |
+ */ |
|
| 7 |
+ function update_state |
|
| 8 |
+ ( |
|
| 9 |
+ model: type_model |
|
| 10 |
+ ): void |
|
| 11 |
+ {
|
|
| 12 |
+ document.querySelector("body").setAttribute("class", model.state);
|
|
| 13 |
+ } |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+ /** |
|
| 17 |
+ * updates the spots (channels and queries) |
|
| 18 |
+ */ |
|
| 19 |
+ function update_spots |
|
| 20 |
+ ( |
|
| 21 |
+ conf: type_conf, |
|
| 22 |
+ model: type_model |
|
| 23 |
+ ): void |
|
| 24 |
+ {
|
|
| 25 |
+ let dom_spots: HTMLUListElement = document.querySelector("#spots");
|
|
| 26 |
+ const spots: Array<type_spot> = ( |
|
| 27 |
+ [] |
|
| 28 |
+ .concat(Object.keys(model.channels).map((name) => ({"kind": "channel", "name": name})))
|
|
| 29 |
+ .concat(Object.keys(model.queries).map((name) => ({"kind": "query", "name": name})))
|
|
| 30 |
+ ); |
|
| 31 |
+ dom_spots.textContent = ""; |
|
| 32 |
+ for (const spot of spots) |
|
| 33 |
+ {
|
|
| 34 |
+ let dom_spot: HTMLLIElement = document.createElement("li");
|
|
| 35 |
+ dom_spot.classList.add("spot");
|
|
| 36 |
+ {
|
|
| 37 |
+ let dom_kind: HTMLSpanElement = document.createElement("span");
|
|
| 38 |
+ dom_kind.classList.add("spot_kind");
|
|
| 39 |
+ dom_kind.textContent = spot.kind; |
|
| 40 |
+ dom_spot.appendChild(dom_kind); |
|
| 41 |
+ } |
|
| 42 |
+ {
|
|
| 43 |
+ let dom_name: HTMLSpanElement = document.createElement("span");
|
|
| 44 |
+ dom_name.classList.add("spot_sender");
|
|
| 45 |
+ dom_name.textContent = spot.name; |
|
| 46 |
+ dom_spot.appendChild(dom_name); |
|
| 47 |
+ } |
|
| 48 |
+ dom_spot.classList.toggle("spot_active", ((spot.kind === model.active.kind) && (spot.name === model.active.name)));
|
|
| 49 |
+ dom_spot.setAttribute("rel", JSON.stringify(spot));
|
|
| 50 |
+ dom_spots.appendChild(dom_spot); |
|
| 51 |
+ } |
|
| 52 |
+ // meeh… |
|
| 53 |
+ ns_control.setup(conf, model); |
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ |
|
| 57 |
+ /** |
|
| 58 |
+ * updates the chat entries |
|
| 59 |
+ */ |
|
| 60 |
+ function update_entries |
|
| 61 |
+ ( |
|
| 62 |
+ model: type_model |
|
| 63 |
+ ): void |
|
| 64 |
+ {
|
|
| 65 |
+ let dom_entries: HTMLUListElement = document.querySelector("#entries");
|
|
| 66 |
+ let entries: Array<type_entry>; |
|
| 67 |
+ switch (model.active.kind) |
|
| 68 |
+ {
|
|
| 69 |
+ case "channel": |
|
| 70 |
+ {
|
|
| 71 |
+ entries = model.channels[model.active.name].entries; |
|
| 72 |
+ break; |
|
| 73 |
+ } |
|
| 74 |
+ case "query": |
|
| 75 |
+ {
|
|
| 76 |
+ entries = model.queries[model.active.name].entries; |
|
| 77 |
+ break; |
|
| 78 |
+ } |
|
| 79 |
+ } |
|
| 80 |
+ dom_entries.textContent = ""; |
|
| 81 |
+ for (const entry of entries) |
|
| 82 |
+ {
|
|
| 83 |
+ let dom_entry: HTMLLIElement = document.createElement("li");
|
|
| 84 |
+ dom_entry.classList.add("entry");
|
|
| 85 |
+ {
|
|
| 86 |
+ let dom_time: HTMLSpanElement = document.createElement("span");
|
|
| 87 |
+ dom_time.classList.add("entry_time");
|
|
| 88 |
+ dom_time.textContent = (new Date(entry.timestamp*1000)).toISOString().slice(11, 19); |
|
| 89 |
+ dom_entry.appendChild(dom_time); |
|
| 90 |
+ } |
|
| 91 |
+ {
|
|
| 92 |
+ let dom_sender: HTMLSpanElement = document.createElement("span");
|
|
| 93 |
+ dom_sender.classList.add("entry_sender");
|
|
| 94 |
+ dom_sender.style.color = get_usercolor(entry.sender); |
|
| 95 |
+ dom_sender.textContent = entry.sender; |
|
| 96 |
+ dom_entry.appendChild(dom_sender); |
|
| 97 |
+ } |
|
| 98 |
+ {
|
|
| 99 |
+ let dom_content: HTMLSpanElement = document.createElement("span");
|
|
| 100 |
+ dom_content.classList.add("entry_content");
|
|
| 101 |
+ dom_content.textContent = entry.content; |
|
| 102 |
+ dom_entry.appendChild(dom_content); |
|
| 103 |
+ } |
|
| 104 |
+ dom_entries.appendChild(dom_entry); |
|
| 105 |
+ } |
|
| 106 |
+ dom_entries.scrollTo(0, dom_entries["scrollTopMax"]); |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ |
|
| 110 |
+ /** |
|
| 111 |
+ * updates the user list |
|
| 112 |
+ */ |
|
| 113 |
+ function update_users |
|
| 114 |
+ ( |
|
| 115 |
+ model: type_model |
|
| 116 |
+ ): void |
|
| 117 |
+ {
|
|
| 118 |
+ let dom_users: HTMLUListElement = document.querySelector("#users");
|
|
| 119 |
+ dom_users.textContent = ""; |
|
| 120 |
+ let users: Array<type_user>; |
|
| 121 |
+ switch (model.active.kind) |
|
| 122 |
+ {
|
|
| 123 |
+ default: |
|
| 124 |
+ {
|
|
| 125 |
+ console.warn("unhandled kind: " + model.active.kind);
|
|
| 126 |
+ users = []; |
|
| 127 |
+ break; |
|
| 128 |
+ } |
|
| 129 |
+ case "channel": |
|
| 130 |
+ {
|
|
| 131 |
+ users = model.channels[model.active.name].users; |
|
| 132 |
+ break; |
|
| 133 |
+ } |
|
| 134 |
+ case "query": |
|
| 135 |
+ {
|
|
| 136 |
+ users = [{"name": model.nickname, "role": ""}, {"name": model.active.name, "role": ""}];
|
|
| 137 |
+ break; |
|
| 138 |
+ } |
|
| 139 |
+ } |
|
| 140 |
+ const users_sorted: Array<type_user> = users.sort |
|
| 141 |
+ ( |
|
| 142 |
+ (x, y) => |
|
| 143 |
+ ( |
|
| 144 |
+ (x.role >= y.role) |
|
| 145 |
+ ? -1 |
|
| 146 |
+ : ( |
|
| 147 |
+ (x.role === y.role) |
|
| 148 |
+ ? ((x.name < y.name) ? -1 : +1) |
|
| 149 |
+ : +1 |
|
| 150 |
+ ) |
|
| 151 |
+ ) |
|
| 152 |
+ ); |
|
| 153 |
+ for (const user of users_sorted) |
|
| 154 |
+ {
|
|
| 155 |
+ let dom_user: HTMLLIElement = document.createElement("li");
|
|
| 156 |
+ dom_user.classList.add("user");
|
|
| 157 |
+ {
|
|
| 158 |
+ let dom_role: HTMLSpanElement = document.createElement("span");
|
|
| 159 |
+ dom_role.textContent = user.role; |
|
| 160 |
+ dom_user.appendChild(dom_role); |
|
| 161 |
+ } |
|
| 162 |
+ {
|
|
| 163 |
+ let dom_name: HTMLSpanElement = document.createElement("span");
|
|
| 164 |
+ dom_name.textContent = user.name; |
|
| 165 |
+ dom_name.style.color = get_usercolor(user.name); |
|
| 166 |
+ dom_user.appendChild(dom_name); |
|
| 167 |
+ } |
|
| 168 |
+ dom_users.appendChild(dom_user); |
|
| 169 |
+ } |
|
| 170 |
+ } |
|
| 171 |
+ |
|
| 172 |
+ |
|
| 173 |
+ /** |
|
| 174 |
+ * clears the content and focus on the message content input |
|
| 175 |
+ */ |
|
| 176 |
+ function clear_content |
|
| 177 |
+ ( |
|
| 178 |
+ ): void |
|
| 179 |
+ {
|
|
| 180 |
+ let dom_content: HTMLInputElement = document.querySelector<HTMLInputElement>("#content");
|
|
| 181 |
+ dom_content.value = ""; |
|
| 182 |
+ dom_content.focus(); |
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ |
|
| 186 |
+ /** |
|
| 187 |
+ * sets up the view |
|
| 188 |
+ */ |
|
| 189 |
+ export function setup |
|
| 190 |
+ ( |
|
| 191 |
+ conf: type_conf, |
|
| 192 |
+ model: type_model |
|
| 193 |
+ ): void |
|
| 194 |
+ {
|
|
| 195 |
+ document.querySelector<HTMLInputElement>("#channel").value = conf.irc.predefined_channel;
|
|
| 196 |
+ document.querySelector<HTMLInputElement>("#nickname").value = (conf.irc.predefined_nickname_prefix + (Math.random()*100).toFixed(0));
|
|
| 197 |
+ |
|
| 198 |
+ ns_model.listen(model, "state_changed", () => {update_state(model);});
|
|
| 199 |
+ ns_model.listen(model, "spots_changed", () => {update_spots(conf, model);});
|
|
| 200 |
+ ns_model.listen(model, "entries_changed", () => {update_entries(model);});
|
|
| 201 |
+ ns_model.listen(model, "users_changed", () => {update_users(model);});
|
|
| 202 |
+ ns_model.listen(model, "message_sent", () => {clear_content();});
|
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+} |
| ... | ... |
@@ -29,7 +29,13 @@ ${dir_build}/index.html: ${dir_source}/index.html
|
| 29 | 29 |
logic: ${dir_build}/logic.js
|
| 30 | 30 |
.PHONY: logic |
| 31 | 31 |
|
| 32 |
-${dir_build}/logic.js: ${dir_source}/logic.ts
|
|
| 32 |
+${dir_build}/logic.js: \
|
|
| 33 |
+ ${dir_source}/types.ts \
|
|
| 34 |
+ ${dir_source}/helpers.ts \
|
|
| 35 |
+ ${dir_source}/model.ts \
|
|
| 36 |
+ ${dir_source}/view.ts \
|
|
| 37 |
+ ${dir_source}/control.ts \
|
|
| 38 |
+ ${dir_source}/main.ts
|
|
| 33 | 39 |
@ ${cmd_log} "logic …"
|
| 34 | 40 |
@ ${cmd_mkdir} $(dir $@)
|
| 35 | 41 |
@ ${cmd_tsc} $^ --outFile $@
|
| 36 | 42 |