1
0
Fork 0

SharedChat manages IRC client + basic formatting (autolinking)

This commit is contained in:
Lephenixnoir 2023-07-25 22:52:56 +02:00
parent 9131c36d63
commit 8bda9f96a9
Signed by untrusted user: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
3 changed files with 160 additions and 28 deletions

View File

@ -18,6 +18,7 @@ body {
--shoutbox-tab-selected-fg: white;
--shoutbox-message-date-color: #949494;
--shoutbox-message-author-color: #777777;
--shoutbox-message-bot-color: gray;
--shoutbox-message-bg1: #ffffff;
--shoutbox-message-bg2: #f8f8f8;
}
@ -143,6 +144,11 @@ body {
word-break: break-word;
white-space: pre-wrap;
}
#v5shoutbox .message[data-author="GLaDOS"] .message-content,
#v5shoutbox .message[data-author="Gitea"] .message-content {
font-style: italic;
color: var(--shoutbox-message-bot-color);
}
#v5shoutbox .message .message-date {
color: var(--shoutbox-message-date-color);

View File

@ -35,7 +35,8 @@ the switch.
import * as IRC from "./v5shoutbox_irc.js";
/* IRC-backed chat running in a shared context (service worker) if possible,
with a default to a local client. */
with a default to a local client. This class also provides nice APIs and
callbacks around the message-passing interface of the IRC client. */
class SharedChat {
/* Registers the service worker that runs the IRC client. Waits for
@ -80,10 +81,14 @@ class SharedChat {
console.log(navigator.serviceWorker);
}
/* Handler called when a message is received from the worker. */
onMessage(e) {
console.log("onMessage!");
console.log(e);
// TODO: Get this via communication
getClientState() {
if(this.irc == null) {
console.error("todo");
return null;
}
else
return new IRC.ClientState(this.irc);
}
async init() {
@ -91,18 +96,42 @@ class SharedChat {
});
console.log("SharedChat service worker registration:", ok);
// if(ok)
// this.irc = null;
// else
// this.irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);
if(ok && false)
this.irc = null;
else {
this.irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);
this.irc.onMessage = (msg) => this.onMessage(msg);
}
}
/** API for sending messages to the IRC client **/
/* Send a message to the IRC client */
remoteCall(e) {
if(this.irc == null)
navigator.serviceWorker.controller.postMessage(e);
else
this.irc.remoteCall(e);
}
connect(login, password) { this.remoteCall({
type: "connect",
login: login,
password: password,
})}
post(channel, message) { this.remoteCall({
type: "post",
channel: channel,
message: message,
})}
/** Handlers for getting messages from the IRC client **/
/* Handler called when a message is received from the worker. */
onMessage(e) {}
}
let sc = new SharedChat();
sc.init();
let irc = new IRC.Client("wss://irc.planet-casio.com:443", undefined);
/* Shoutbox entry point and DOM manipulation. */
let shoutbox = new function() {
@ -119,8 +148,10 @@ let shoutbox = new function() {
/* Last observed client state */
this.clientState = undefined;
this.init = function(root) {
this.init = function(root, chat) {
this.title = document.title;
this.chat = chat;
this.chat.onMessage = (e) => this.onMessage(e);
/* Channel views */
this.eChannels = root.querySelector(".channels");
@ -131,13 +162,17 @@ let shoutbox = new function() {
this.eStatus = root.querySelector(".status");
/* Elements of the login form */
this.eLoginForm = root.querySelector(".login-form");
this.eConnect = root.querySelector(".connect");
this.eLogin = root.querySelector(".login");
this.ePassword = root.querySelector(".password");
/* Elements of the shoutbox form */
this.eShoutboxForm = root.querySelector(".shoutbox-form");
this.eMessage = root.querySelector(".message");
/* Do NOT submit a form and reload the page, even if the script fails,
thank you. I like to debug things. */
this.eLoginForm.action = "javascript:void(0)";
this.eShoutboxForm.action = "javascript:void(0)";
this.eLoginForm.addEventListener("submit", e => {
e.preventDefault();
shoutbox.connect();
@ -162,15 +197,14 @@ let shoutbox = new function() {
shoutbox.focused = false;
});
// TODO: Get this via communication
this.clientState = new IRC.ClientState(irc);
this.clientState = this.chat.getClientState();
console.log(this.clientState);
this.selectChannel("\\login");
this.refreshView();
}
/*** IRC callbacks ***/
irc.onMessage = (e) => this.onMessage(e);
this.onMessage = function(e) {
/* Update known client state based on info bundled with the message */
if(e.state !== undefined)
@ -199,8 +233,10 @@ let shoutbox = new function() {
}
function log(message) {
/* TODO: Use a better text element x) */
this.eLog.innerHTML += message + "\n";
/* Most consistent escaping method */
const span = document.createElement("span");
span.textContent = message + "\n";
this.eLog.appendChild(span);
}
this.log = log.bind(this);
@ -234,8 +270,10 @@ let shoutbox = new function() {
this.eLoginTab.style.display = running ? "none" : "inline-block";
const name = this.channel;
for(const e of this.eChannels.children)
e.style.display = (e.dataset.channel === name) ? "flex" : "none";
for(const e of this.eChannels.children) {
const visible_mode = (e.dataset.channel == "\\log" ? "block" : "flex");
e.style.display = (e.dataset.channel === name) ? visible_mode : "none";
}
for(const b of this.eChannelButtons.children) {
if(b.dataset.channel === name)
b.classList.add("current");
@ -307,6 +345,74 @@ let shoutbox = new function() {
shoutbox.selectChannel(channel);
}
this.messageToNode = function(message, element) {
const rURL = /(https?:\/\/|ftp:\/\/|magnet:)/d;
const fURL = (match) => {
/* We've found the protocol, now read the arguments until whitespace or
unbalanced closing parenthesis */
let i = match.indices[0][1];
let par_depth = 0;
while(i < match.input.length && !/\s/.test(match.input[i])) {
par_depth += (match.input[i] == "(");
par_depth -= (match.input[i] == ")");
if(par_depth < 0)
break;
i++;
}
/* Don't count the last character if it's a quote or comma */
if(i > 0 && /[",]/.test(match.input[i-1]))
i--;
const url = match.input.substring(match.indices[0][0], i);
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.appendChild(document.createTextNode(url));
element.appendChild(a);
return url.length;
};
/* List of matchers: regex, handling function, match object, index. */
let matchers = [
[rURL, fURL, null, -1],
];
/* Repeatedly find the next segment to convert. */
let i = 0;
while(i < message.length) {
let next = message.length;
let next_matcher = null;
/* Update the next matches for all regexes and find the one that matches
the earliest. */
for(const m of matchers) {
if(m[3] < 0) {
m[0].lastIndex = 0;
m[2] = m[0].exec(message.substring(i));
m[3] = (m[2] !== null) ? i + m[2].index : message.length;
}
if(m[3] >= 0 && m[3] < next) {
next = m[3];
next_matcher = m;
}
}
/* Find the closest one. If it's not at offset 0, do a text node. */
if(next > i) {
const sub = message.substring(i, next);
element.appendChild(document.createTextNode(sub));
i = next;
}
if(next_matcher !== null) {
i += next_matcher[1](next_matcher[2]);
next_matcher[2] = null;
next_matcher[3] = -1;
}
}
}
this.addNewMessage = function(channel, date, author, message) {
const view = this.getChannelView(channel);
if(view === undefined)
@ -318,13 +424,14 @@ let shoutbox = new function() {
let messageElement = document.createElement("div");
messageElement.classList.add("message");
messageElement.dataset.author = author;
let authorElement = document.createElement("div");
authorElement.appendChild(document.createTextNode(author));
authorElement.classList.add("message-author");
messageElement.appendChild(authorElement);
let messageContentElement = document.createElement("p");
messageContentElement.classList.add("message-content");
messageContentElement.appendChild(document.createTextNode(message));
this.messageToNode(message, messageContentElement);
messageElement.appendChild(messageContentElement);
let dateElement = document.createElement("div");
dateElement.classList.add("message-date");
@ -353,7 +460,7 @@ let shoutbox = new function() {
if(this.eLogin.value.includes(" "))
return this.log("Login should not contain a space!");
irc.connect(this.eLogin.value, this.ePassword.value);
this.chat.connect(this.eLogin.value, this.ePassword.value);
}
this.selectChannel = function(name) {
@ -370,11 +477,21 @@ let shoutbox = new function() {
if(!this.isOnRemoteChannel())
return log("Cannot send message as no channel is selected!");
irc.postMessage(this.channel, this.eMessage.value);
this.chat.post(this.channel, this.eMessage.value);
this.eMessage.value = "";
}
};
document.addEventListener("DOMContentLoaded", function() {
shoutbox.init(document.getElementById("v5shoutbox"));
/* We initialize the shoutbox once the SharedChat has finished its async init
*and* the DOMContentLoaded even has been fired. */
let sc = new SharedChat();
const sc_init_promise = sc.init();
document.addEventListener("DOMContentLoaded", () => {
sc_init_promise.then(() => {
shoutbox.init(document.getElementById("v5shoutbox"), sc);
});
});
await sc_init_promise;

View File

@ -181,6 +181,15 @@ export class Client {
this.sendMessage({ type: "channel_changed", channel: channel });
}
/*** Message-based actions ***/
remoteCall(e) {
if(e.type == "connect")
this.connect(e.login, e.password);
else if(e.type == "post")
this.postMessage(e.channel, e.message);
}
/*** Connection management ***/
setState(state) {