Initial commit
This commit is contained in:
212
assets/client.js
Normal file
212
assets/client.js
Normal file
@@ -0,0 +1,212 @@
|
||||
var server = {
|
||||
name: "chat.freenode.net",
|
||||
url: "ws://localhost:8080",
|
||||
username: "soju-test-user/irc.freenode.net",
|
||||
realname: "soju-test-user",
|
||||
nick: "soju-test-user",
|
||||
pass: "soju-test-user",
|
||||
};
|
||||
|
||||
var buffers = {};
|
||||
var activeBuffer = null;
|
||||
|
||||
var bufferListElt = document.getElementById("buffer-list");
|
||||
var logElt = document.getElementById("log");
|
||||
var composerElt = document.getElementById("composer");
|
||||
var composerInputElt = document.getElementById("composer-input");
|
||||
|
||||
function createMessageElement(msg) {
|
||||
var date = new Date();
|
||||
|
||||
var line = document.createElement("div");
|
||||
line.className = "logline";
|
||||
|
||||
var timestamp = document.createElement("a");
|
||||
timestamp.href = "#";
|
||||
timestamp.className = "timestamp";
|
||||
timestamp.innerText = date.toLocaleTimeString(undefined, {
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
timestamp.onclick = function(event) {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
line.appendChild(timestamp);
|
||||
|
||||
switch (msg.command) {
|
||||
case "NOTICE":
|
||||
case "PRIVMSG":
|
||||
var text = msg.params[1];
|
||||
|
||||
var nick = document.createElement("a");
|
||||
nick.href = "#";
|
||||
nick.className = "nick";
|
||||
nick.innerText = msg.prefix.name;
|
||||
nick.onclick = function(event) {
|
||||
event.preventDefault();
|
||||
switchBuffer(createBuffer(msg.prefix.name));
|
||||
};
|
||||
|
||||
line.appendChild(document.createTextNode(" <"));
|
||||
line.appendChild(nick);
|
||||
line.appendChild(document.createTextNode("> "));
|
||||
line.appendChild(document.createTextNode(text));
|
||||
break;
|
||||
default:
|
||||
line.appendChild(document.createTextNode(" " + msg.command + " " + msg.params.join(" ")));
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
function createBuffer(name) {
|
||||
if (buffers[name]) {
|
||||
return buffers[name];
|
||||
}
|
||||
|
||||
var a = document.createElement("a");
|
||||
a.href = "#";
|
||||
a.onclick = function(event) {
|
||||
event.preventDefault();
|
||||
switchBuffer(name);
|
||||
};
|
||||
a.innerText = name;
|
||||
|
||||
var li = document.createElement("li");
|
||||
li.appendChild(a);
|
||||
|
||||
buf = {
|
||||
name: name,
|
||||
li: li,
|
||||
messages: [],
|
||||
readOnly: false,
|
||||
|
||||
addMessage: function(msg) {
|
||||
buf.messages.push(msg);
|
||||
|
||||
if (activeBuffer == buf) {
|
||||
logElt.appendChild(createMessageElement(msg));
|
||||
}
|
||||
},
|
||||
};
|
||||
buffers[name] = buf;
|
||||
|
||||
bufferListElt.appendChild(li);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function switchBuffer(buf) {
|
||||
if (typeof buf == "string") {
|
||||
buf = buffers[buf];
|
||||
}
|
||||
if (activeBuffer && buf === activeBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeBuffer) {
|
||||
activeBuffer.li.classList.remove("active");
|
||||
}
|
||||
|
||||
activeBuffer = buf;
|
||||
if (!buf) {
|
||||
return;
|
||||
}
|
||||
|
||||
buf.li.classList.add("active");
|
||||
|
||||
logElt.innerHTML = "";
|
||||
for (var msg of buf.messages) {
|
||||
logElt.appendChild(createMessageElement(msg));
|
||||
}
|
||||
|
||||
composerElt.classList.toggle("read-only", buf.readOnly);
|
||||
if (!buf.readOnly) {
|
||||
composerInputElt.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessage(target, date, nickname, text) {
|
||||
var timestamp = document.createElement("a");
|
||||
timestamp.href = "#";
|
||||
timestamp.className = "timestamp";
|
||||
timestamp.innerText = date.toLocaleTimeString(undefined, {
|
||||
timeStyle: "short",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
var nick = document.createElement("a");
|
||||
nick.href = "#";
|
||||
nick.className = "nick";
|
||||
nick.innerText = nickname;
|
||||
|
||||
var line = document.createElement("div");
|
||||
line.className = "logline";
|
||||
line.appendChild(timestamp);
|
||||
line.appendChild(document.createTextNode(" <"));
|
||||
line.appendChild(nick);
|
||||
line.appendChild(document.createTextNode("> "));
|
||||
line.appendChild(document.createTextNode(text));
|
||||
logElt.appendChild(line);
|
||||
}
|
||||
|
||||
var serverBuffer = createBuffer(server.name);
|
||||
serverBuffer.readOnly = true;
|
||||
switchBuffer(serverBuffer);
|
||||
|
||||
var ws = new WebSocket(server.url);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log("Connection opened");
|
||||
|
||||
if (server.pass) {
|
||||
ws.send(formatMessage({ command: "PASS", params: [server.pass] }));
|
||||
}
|
||||
ws.send(formatMessage({ command: "NICK", params: [server.nick] }));
|
||||
ws.send(formatMessage({
|
||||
command: "USER",
|
||||
params: [server.username, "0", "*", server.realname],
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
var msg = parseMessage(event.data);
|
||||
console.log(msg);
|
||||
|
||||
switch (msg.command) {
|
||||
case "NOTICE":
|
||||
case "PRIVMSG":
|
||||
var target = msg.params[0];
|
||||
if (target == server.nick) {
|
||||
target = msg.prefix.name;
|
||||
}
|
||||
createBuffer(target).addMessage(msg);
|
||||
break;
|
||||
case "JOIN":
|
||||
var channel = msg.params[0];
|
||||
if (msg.prefix.name == server.nick) {
|
||||
createBuffer(channel);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
serverBuffer.addMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log("Connection closed");
|
||||
};
|
||||
|
||||
composerElt.onsubmit = function(event) {
|
||||
event.preventDefault();
|
||||
if (!activeBuffer || activeBuffer.readOnly) {
|
||||
return;
|
||||
}
|
||||
var target = activeBuffer.name;
|
||||
var text = composerInputElt.value;
|
||||
var msg = { command: "PRIVMSG", params: [target, text] };
|
||||
ws.send(formatMessage(msg));
|
||||
msg.prefix = { name: server.nick };
|
||||
activeBuffer.addMessage(msg);
|
||||
composerInputElt.value = "";
|
||||
};
|
||||
104
assets/irc.js
Normal file
104
assets/irc.js
Normal file
@@ -0,0 +1,104 @@
|
||||
function parsePrefix(s) {
|
||||
var prefix = {
|
||||
name: null,
|
||||
user: null,
|
||||
host: null,
|
||||
};
|
||||
|
||||
var i = s.indexOf("@");
|
||||
if (i < 0) {
|
||||
prefix.name = s;
|
||||
return prefix;
|
||||
}
|
||||
prefix.host = s.slice(i + 1);
|
||||
s = s.slice(0, i);
|
||||
|
||||
var i = s.indexOf("!");
|
||||
if (i < 0) {
|
||||
prefix.name = s;
|
||||
return prefix;
|
||||
}
|
||||
prefix.name = s.slice(0, i);
|
||||
prefix.user = s.slice(i + 1);
|
||||
return prefix;
|
||||
}
|
||||
|
||||
function formatPrefix(prefix) {
|
||||
if (!prefix.host) {
|
||||
return prefix.name;
|
||||
}
|
||||
if (!prefix.user) {
|
||||
return prefix.name + "@" + prefix.host;
|
||||
}
|
||||
return prefix.name + "!" + prefix.user + "@" + prefix.host;
|
||||
}
|
||||
|
||||
function parseMessage(s) {
|
||||
if (s.endsWith("\r\n")) {
|
||||
s = s.slice(0, s.length - 2);
|
||||
}
|
||||
|
||||
var msg = {
|
||||
prefix: null,
|
||||
command: null,
|
||||
params: [],
|
||||
};
|
||||
|
||||
if (s.startsWith("@")) {
|
||||
// TODO: parse tags
|
||||
}
|
||||
|
||||
if (s.startsWith(":")) {
|
||||
var parts = s.split(" ", 2);
|
||||
var i = s.indexOf(" ");
|
||||
if (i < 0) {
|
||||
throw new Error("expected a space after prefix");
|
||||
}
|
||||
msg.prefix = parsePrefix(s.slice(1, i));
|
||||
s = s.slice(i + 1);
|
||||
}
|
||||
|
||||
var i = s.indexOf(" ");
|
||||
if (i < 0) {
|
||||
msg.command = s;
|
||||
return msg;
|
||||
}
|
||||
msg.command = s.slice(0, i);
|
||||
s = s.slice(i + 1);
|
||||
|
||||
while (true) {
|
||||
if (s.startsWith(":")) {
|
||||
msg.params.push(s.slice(1));
|
||||
break;
|
||||
}
|
||||
|
||||
i = s.indexOf(" ");
|
||||
if (i < 0) {
|
||||
msg.params.push(s);
|
||||
break;
|
||||
}
|
||||
|
||||
msg.params.push(s.slice(0, i));
|
||||
s = s.slice(i + 1);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
function formatMessage(msg) {
|
||||
var s = "";
|
||||
// TODO: format tags
|
||||
if (msg.prefix) {
|
||||
s += ":" + formatPrefix(msg.prefix) + " ";
|
||||
}
|
||||
s += msg.command;
|
||||
if (msg.params && msg.params.length > 0) {
|
||||
var last = msg.params[msg.params.length - 1];
|
||||
if (msg.params.length > 1) {
|
||||
s += " " + msg.params.slice(0, -1).join(" ");
|
||||
}
|
||||
s += " :" + last;
|
||||
}
|
||||
s += "\r\n";
|
||||
return s;
|
||||
}
|
||||
75
assets/style.css
Normal file
75
assets/style.css
Normal file
@@ -0,0 +1,75 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-rows: auto 40px;
|
||||
grid-template-columns: 200px auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#sidebar, #log {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
background-color: #e3e3e3;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
#sidebar ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
#sidebar ul a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#log {
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#composer {
|
||||
grid-column: 2;
|
||||
border-top: 1px solid #e3e3e3;
|
||||
}
|
||||
#composer input {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
#composer.read-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: green;
|
||||
}
|
||||
#sidebar a, a.timestamp, a.nick {
|
||||
color: #4a4a4a;
|
||||
text-decoration: none;
|
||||
}
|
||||
#sidebar .active a {
|
||||
color: black;
|
||||
}
|
||||
#sidebar a:hover, #sidebar a:active,
|
||||
a.timestamp:hover, a.timestamp:active,
|
||||
a.nick:hover, a.nick:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.nick {
|
||||
color: #f25e0d;
|
||||
}
|
||||
Reference in New Issue
Block a user