432 lines
13 KiB
JavaScript
432 lines
13 KiB
JavaScript
/* User Interface class for the entire shoutbox. */
|
|
class UIShoutbox {
|
|
|
|
constructor(shc, root) {
|
|
// TODO: Better handle the "available channels" limit (in IRCClient?)
|
|
this.availableChannels = ["#annonces", "#projets", "#hs"];
|
|
this.shc = shc;
|
|
|
|
/* Original tab title */
|
|
this.title = document.title;
|
|
/* Whether we currently have focus */
|
|
this.focused = true;
|
|
/* Number of messages received since last losing focus */
|
|
this.newMessages = 0;
|
|
/* Current channel */
|
|
this.channel = "\\login";
|
|
|
|
/*** Get DOM elements ***/
|
|
|
|
this.eRoot = root;
|
|
this.eStatus = root.querySelector(".status");
|
|
/* Channel views */
|
|
this.eChannels = root.querySelector(".channels");
|
|
this.eChannelButtons = root.querySelector(".channel-buttons");
|
|
this.eLoginTab = root.querySelector(".tab-login");
|
|
/* Elements of the login form */
|
|
this.eLoginForm = root.querySelector(".login-form");
|
|
this.eLogin = root.querySelector(".login");
|
|
this.ePassword = root.querySelector(".password");
|
|
this.eAuthenticationError = root.querySelector(".error");
|
|
/* Elements of the shoutbox form */
|
|
this.eShoutboxForm = root.querySelector(".shoutbox-form");
|
|
this.eMessage = root.querySelector(".message-input");
|
|
|
|
/*** Sub-UI ***/
|
|
|
|
this.uiDebug = new UIDebug(shc,
|
|
root.querySelector(".tab-debug"),
|
|
root.querySelector(".debugchannel"));
|
|
|
|
/*** Event setups ***/
|
|
|
|
/* 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();
|
|
this.eventConnect();
|
|
});
|
|
this.eShoutboxForm.addEventListener("submit", e => {
|
|
e.preventDefault();
|
|
this.eventPost();
|
|
});
|
|
root.querySelector(".tab-login").addEventListener("click", () => {
|
|
this.selectChannel("\\login");
|
|
});
|
|
root.querySelector(".tab-settings").addEventListener("click", () => {
|
|
this.selectChannel("\\settings");
|
|
});
|
|
root.querySelector(".tab-debug").addEventListener("click", () => {
|
|
this.selectChannel("\\debug");
|
|
});
|
|
|
|
window.addEventListener("focus", () => {
|
|
this.focused = true;
|
|
this.newMessages = 0;
|
|
this.refreshTitle();
|
|
});
|
|
window.addEventListener("blur", () => {
|
|
this.focused = false;
|
|
});
|
|
|
|
/*** Settings updates ***/
|
|
|
|
const settingShowDebug = root.querySelector(".setting-show-debug");
|
|
settingShowDebug.checked = false;
|
|
settingShowDebug.addEventListener("change", e => {
|
|
this.uiDebug.setTabVisibility(e.target.checked);
|
|
});
|
|
|
|
/*** Callbacks ***/
|
|
|
|
this.shc.onDebugLog = (ctgy, message) =>
|
|
this.uiDebug.logMessage(ctgy, message);
|
|
|
|
this.shc.onStateChanged = () =>
|
|
this.refreshView();
|
|
|
|
this.shc.onIRCChannelChanged = (channel, info) =>
|
|
this.refreshView();
|
|
|
|
this.shc.onIRCAuthResult = (successful) => {
|
|
this.ePassword.value = "";
|
|
this.eAuthenticationError.style.display = successful ? "none" : "block";
|
|
if(successful)
|
|
this.getAllHistory();
|
|
}
|
|
|
|
/* Execute the welcome eagerly if we've already been welcomed! */
|
|
if(this.shc.state().isLocallySetup())
|
|
this.getAllHistory();
|
|
this.shc.onWelcome = () =>
|
|
this.getAllHistory();
|
|
|
|
this.shc.onIRCNewMessage = this.handleIRCNewMessage.bind(this);
|
|
this.shc.onIRCHistoryReceived = this.handleIRCHistoryReceived.bind(this);
|
|
|
|
this.refreshView();
|
|
}
|
|
|
|
isOnRemoteChannel() {
|
|
const c = this.channel;
|
|
return (c !== null) && (c.startsWith("&") || c.startsWith("#"));
|
|
}
|
|
|
|
/*** View refresh mechanism ***/
|
|
|
|
refreshTitle() {
|
|
if(this.newMessages === 0)
|
|
document.title = this.title;
|
|
else
|
|
document.title = `(${this.newMessages}) ${this.title}`;
|
|
}
|
|
|
|
refreshView() {
|
|
const state = this.shc.state();
|
|
const running = state.isRunning();
|
|
const activating =
|
|
this.eRoot.classList.contains("notrunning") && running;
|
|
|
|
if(state.irc !== null) {
|
|
for(const [channel, info] of state.irc.channels)
|
|
this.refreshChannel(channel, activating);
|
|
}
|
|
|
|
const viewingChannel = this.isOnRemoteChannel();
|
|
|
|
/* Update connection information at root */
|
|
if(!running)
|
|
this.eRoot.classList.add("notrunning");
|
|
else
|
|
this.eRoot.classList.remove("notrunning");
|
|
|
|
/* Update status/channel line */
|
|
if(running && viewingChannel) {
|
|
const c = this.channel;
|
|
this.eStatus.textContent = " : " + state.irc.channels.get(c).header;
|
|
let code = document.createElement("code");
|
|
code.appendChild(document.createTextNode(c));
|
|
this.eStatus.insertBefore(code, this.eStatus.childNodes[0]);
|
|
}
|
|
else {
|
|
this.eStatus.textContent =
|
|
state.SHCConnString() + " · " + state.IRCConnString();
|
|
}
|
|
|
|
/* Update visibility of forms */
|
|
this.eShoutboxForm.style.display =
|
|
running && viewingChannel ? "flex" : "none";
|
|
this.eLoginTab.style.display =
|
|
running ? "none" : "inline-block";
|
|
|
|
/* Update which tab's contents is visible */
|
|
for(const e of this.eChannels.children) {
|
|
const normal = (e.dataset.channel === "\\settings") ? "block" : "flex";
|
|
e.style.display = (e.dataset.channel === this.channel) ? normal : "none";
|
|
}
|
|
|
|
/* Update tab widgets' properties */
|
|
for(const b of this.eChannelButtons.children) {
|
|
if(b.dataset.channel === this.channel)
|
|
b.classList.add("current");
|
|
else
|
|
b.classList.remove("current");
|
|
}
|
|
|
|
/* Update status table */
|
|
this.uiDebug.refreshView();
|
|
}
|
|
|
|
refreshChannel(channel, activating) {
|
|
if(!this.availableChannels.includes(channel))
|
|
return;
|
|
|
|
if(this.getChannelView(channel) === undefined) {
|
|
activating = true;
|
|
let view = document.createElement("div");
|
|
view.classList.add("channel");
|
|
view.dataset.channel = channel;
|
|
view.style.display = "none";
|
|
this.eChannels.appendChild(view);
|
|
|
|
let button = document.createElement("a");
|
|
button.classList.add("tab");
|
|
button.classList.add("tab-channel");
|
|
button.appendChild(document.createTextNode(channel));
|
|
button.dataset.channel = channel;
|
|
this.addChannelButton(button);
|
|
|
|
button.addEventListener("click", () => {
|
|
this.selectChannel(button.dataset.channel);
|
|
});
|
|
}
|
|
|
|
/* Automatically select channels when they appear. */
|
|
if(activating && (this.channel === null || this.channel === "\\login"))
|
|
this.selectChannel(channel);
|
|
}
|
|
|
|
/*** Event handlers ***/
|
|
|
|
eventConnect() {
|
|
if(this.eLogin.value === "" || this.ePassword.value === "" ||
|
|
this.shc.state().isTalkingToIRCServer())
|
|
return;
|
|
if(this.eLogin.value.includes(" ")) {
|
|
this.uiDebug.logMessage("UI", "Login should not contain a space!");
|
|
return;
|
|
}
|
|
|
|
this.shc.connect(this.eLogin.value, this.ePassword.value);
|
|
}
|
|
|
|
selectChannel(name) {
|
|
this.channel = name;
|
|
this.setChannelBackgroundActivity(name, false);
|
|
this.refreshView();
|
|
}
|
|
|
|
eventPost() {
|
|
if(this.eMessage.value === "" || !this.shc.state().isRunning() ||
|
|
!this.isOnRemoteChannel())
|
|
return;
|
|
|
|
this.shc.postMessage(this.channel, this.eMessage.value);
|
|
this.eMessage.value = "";
|
|
}
|
|
|
|
handleIRCNewMessage(id, channel, date, author, message) {
|
|
this.addNewMessage(id, channel, date, author, message);
|
|
|
|
if(channel === this.channel && !this.focused) {
|
|
this.newMessages++;
|
|
this.refreshTitle();
|
|
}
|
|
if(channel !== this.channel) {
|
|
this.setChannelBackgroundActivity(channel, true);
|
|
}
|
|
}
|
|
|
|
handleIRCHistoryReceived(channel, messages) {
|
|
messages.forEach(msg => this.addNewMessage(
|
|
msg.id, msg.channel, msg.date, msg.source, msg.message, true));
|
|
}
|
|
|
|
getAllHistory() {
|
|
const state = this.shc.state();
|
|
console.log("Getting history:", state);
|
|
// TODO: Get history also queries shoutbox-excluded channels
|
|
if(state.isRunning())
|
|
state.irc.channels.forEach((_, c) => this.shc.getHistory(c));
|
|
}
|
|
|
|
/*** DOM manipulation ***/
|
|
|
|
getChannelView(channel) {
|
|
return Array.from(this.eChannels.children)
|
|
.find(e => e.dataset.channel === channel);
|
|
}
|
|
|
|
getChannelButton(channel) {
|
|
return Array.from(this.eChannelButtons.children)
|
|
.find(e => e.dataset.channel === channel);
|
|
}
|
|
|
|
addChannelButton(button) {
|
|
/* Insert new tab button before special tabs */
|
|
let firstSpecial = Array.from(this.eChannelButtons.children).find(e =>
|
|
e.dataset.channel.startsWith("\\")) || null;
|
|
this.eChannelButtons.insertBefore(button, firstSpecial);
|
|
}
|
|
|
|
clearChannelButtons(button) {
|
|
let special = Array.from(this.eChannelButtons.children).filter(e =>
|
|
e.dataset.channel.startsWith("\\"));
|
|
this.eChannelButtons.replaceChildren(special);
|
|
}
|
|
|
|
addNewMessage(id, channel, date, author, message, history=false) {
|
|
const view = this.getChannelView(channel);
|
|
if(view === undefined)
|
|
return;
|
|
|
|
for(const msg of view.children) {
|
|
if(msg.dataset.id === id)
|
|
return;
|
|
}
|
|
|
|
/* Remove the shoutbox bridge's "[s]" suffix */
|
|
if(author.endsWith("[s]"))
|
|
author = author.substr(0, author.length - 3);
|
|
|
|
let messageElement = document.createElement("div");
|
|
messageElement.classList.add("message");
|
|
if(history)
|
|
messageElement.classList.add("history");
|
|
messageElement.dataset.author = author;
|
|
messageElement.dataset.id = id;
|
|
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");
|
|
const fmt = new IRCFormatter();
|
|
fmt.format(message, messageContentElement);
|
|
messageElement.appendChild(messageContentElement);
|
|
let dateElement = document.createElement("div");
|
|
dateElement.classList.add("message-date");
|
|
dateElement.appendChild(document.createTextNode(date.toLocaleTimeString()));
|
|
messageElement.appendChild(dateElement);
|
|
view.prepend(messageElement);
|
|
}
|
|
|
|
setChannelBackgroundActivity(channel, activity) {
|
|
const button = this.getChannelButton(channel);
|
|
if(button !== undefined) {
|
|
if(activity)
|
|
button.classList.add("bg-activity");
|
|
else
|
|
button.classList.remove("bg-activity");
|
|
}
|
|
}
|
|
};
|
|
|
|
/* DOM manipulation class handling the debug tab of the client. */
|
|
class UIDebug {
|
|
constructor(shc, tabButton, root) {
|
|
this.shc = shc;
|
|
|
|
/* UI elements */
|
|
this.eTab = tabButton;
|
|
this.eLog = root.querySelector(".log pre");
|
|
this.eStatusTable = root.querySelector("table");
|
|
|
|
/* Log filters */
|
|
this.eFilterIRC = root.querySelector(".debug-log-irc");
|
|
this.eFilterWorker = root.querySelector(".debug-log-worker");
|
|
this.eFilterOther = root.querySelector(".debug-log-other");
|
|
|
|
this.eFilterIRC.checked = true;
|
|
this.eFilterWorker.checked = true;
|
|
this.eFilterOther.checked = true;
|
|
|
|
this.eFilterIRC.addEventListener("change", () =>
|
|
this.runLogFilter());
|
|
this.eFilterWorker.addEventListener("change", () =>
|
|
this.runLogFilter());
|
|
this.eFilterOther.addEventListener("change", () =>
|
|
this.runLogFilter());
|
|
|
|
/* Button actions */
|
|
root.querySelector(".action-disconnect").addEventListener("click", () => {
|
|
this.shc.disconnect();
|
|
});
|
|
root.querySelector(".get-history").addEventListener("click", () => {
|
|
const state = this.shc.state();
|
|
// TODO: Get history debug also queries shoutbox-excluded channels
|
|
if(state.isRunning())
|
|
state.irc.channels.forEach((_, c) => this.shc.getHistory(c));
|
|
});
|
|
}
|
|
|
|
/* Set whether the debug tab should be visible. */
|
|
setTabVisibility(visible) {
|
|
this.eTab.style.display = visible ? "inline-block" : "none";
|
|
}
|
|
|
|
/* Add a message to the debug log. Category can be `ircin`, `ircout`,
|
|
`shcin`, `shcout`, or a general categor string. */
|
|
logMessage(ctgy, message) {
|
|
const line = document.createElement("div");
|
|
if(ctgy !== "")
|
|
line.classList.add(ctgy);
|
|
|
|
/* Most consistent escaping method */
|
|
line.appendChild(document.createTextNode(message));
|
|
this.eLog.appendChild(line);
|
|
}
|
|
|
|
refreshView() {
|
|
const state = this.shc.state();
|
|
var props = [
|
|
["SHCConn", state.SHCConnString()],
|
|
["IRCConn", state.IRCConnString()],
|
|
];
|
|
|
|
while(this.eStatusTable.firstChild)
|
|
this.eStatusTable.removeChild(this.eStatusTable.lastChild);
|
|
for(const [name, value] of props) {
|
|
var tr = document.createElement("tr");
|
|
var td1 = document.createElement("td");
|
|
var td2 = document.createElement("td");
|
|
td1.appendChild(document.createTextNode(name));
|
|
td2.appendChild(document.createTextNode(value));
|
|
tr.appendChild(td1);
|
|
tr.appendChild(td2);
|
|
this.eStatusTable.appendChild(tr);
|
|
}
|
|
}
|
|
|
|
runLogFilter() {
|
|
const enabled = [
|
|
this.eFilterIRC.checked,
|
|
this.eFilterWorker.checked,
|
|
this.eFilterOther.checked,
|
|
];
|
|
const classToCategory = {
|
|
ircin: 0, ircout: 0,
|
|
shcin: 1, shcout: 1,
|
|
};
|
|
|
|
for(const child of this.eLog.children) {
|
|
let cat = classToCategory[child.className];
|
|
cat = (cat === undefined) ? 2 : cat;
|
|
child.style.display = enabled[cat] ? "block" : "none";
|
|
}
|
|
}
|
|
}
|