forked from devs/v5shoutbox
SharedChat manages IRC client + basic formatting (autolinking)
This commit is contained in:
parent
9131c36d63
commit
8bda9f96a9
|
@ -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);
|
||||
|
|
173
v5shoutbox.js
173
v5shoutbox.js
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue