forked from CringeStudios/gamja
Switch to react
Under the hood, preact is used to reduce dependency size. We still don't have a build stage, so htm is used instead of JSX.
This commit is contained in:
parent
62300746d3
commit
b449ace4b4
388
components/app.js
Normal file
388
components/app.js
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
import * as irc from "/lib/irc.js";
|
||||||
|
import Client from "/lib/client.js";
|
||||||
|
import Buffer from "/components/buffer.js";
|
||||||
|
import BufferList from "/components/buffer-list.js";
|
||||||
|
import Connect from "/components/connect.js";
|
||||||
|
import Composer from "/components/composer.js";
|
||||||
|
import { html, Component, createRef } from "/lib/index.js";
|
||||||
|
|
||||||
|
const SERVER_BUFFER = "*";
|
||||||
|
|
||||||
|
const DISCONNECTED = "disconnected";
|
||||||
|
const CONNECTING = "connecting";
|
||||||
|
const REGISTERED = "registered";
|
||||||
|
|
||||||
|
function parseQueryString() {
|
||||||
|
var query = window.location.search.substring(1);
|
||||||
|
var params = {};
|
||||||
|
query.split('&').forEach((s) => {
|
||||||
|
if (!s) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pair = s.split('=');
|
||||||
|
params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class App extends Component {
|
||||||
|
client = null;
|
||||||
|
state = {
|
||||||
|
connectParams: {
|
||||||
|
serverURL: null,
|
||||||
|
serverPass: null,
|
||||||
|
username: null,
|
||||||
|
realname: null,
|
||||||
|
nick: null,
|
||||||
|
saslPlain: null,
|
||||||
|
autojoin: [],
|
||||||
|
},
|
||||||
|
status: DISCONNECTED,
|
||||||
|
buffers: new Map(),
|
||||||
|
activeBuffer: null,
|
||||||
|
};
|
||||||
|
composer = createRef();
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleConnectSubmit = this.handleConnectSubmit.bind(this);
|
||||||
|
this.handleBufferListClick = this.handleBufferListClick.bind(this);
|
||||||
|
this.handleComposerSubmit = this.handleComposerSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBufferState(name, updater, callback) {
|
||||||
|
this.setState((state) => {
|
||||||
|
var buf = state.buffers.get(name);
|
||||||
|
if (!buf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newBuf = updater(buf);
|
||||||
|
if (buf === newBuf || !newBuf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffers = new Map(state.buffers);
|
||||||
|
buffers.set(name, newBuf);
|
||||||
|
return { buffers };
|
||||||
|
}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
createBuffer(name) {
|
||||||
|
this.setState((state) => {
|
||||||
|
if (state.buffers.get(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffers = new Map(state.buffers);
|
||||||
|
buffers.set(name, {
|
||||||
|
name: name,
|
||||||
|
topic: null,
|
||||||
|
members: new Map(),
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
return { buffers };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switchBuffer(name) {
|
||||||
|
this.setState({ activeBuffer: name }, () => {
|
||||||
|
if (this.composer.current) {
|
||||||
|
this.composer.current.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(bufName, msg) {
|
||||||
|
if (!msg.tags) {
|
||||||
|
msg.tags = {};
|
||||||
|
}
|
||||||
|
// TODO: set time tag if missing
|
||||||
|
|
||||||
|
this.createBuffer(bufName);
|
||||||
|
this.setBufferState(bufName, (buf) => {
|
||||||
|
return {
|
||||||
|
...buf,
|
||||||
|
messages: buf.messages.concat(msg),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(params) {
|
||||||
|
this.setState({ status: CONNECTING, connectParams: params });
|
||||||
|
|
||||||
|
this.client = new Client({
|
||||||
|
url: params.serverURL,
|
||||||
|
pass: params.serverPass,
|
||||||
|
nick: params.nick,
|
||||||
|
username: params.username,
|
||||||
|
realname: params.realname,
|
||||||
|
saslPlain: params.saslPlain,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.addEventListener("close", () => {
|
||||||
|
this.setState({ status: DISCONNECTED });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.addEventListener("message", (event) => {
|
||||||
|
this.handleMessage(event.detail.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createBuffer(SERVER_BUFFER);
|
||||||
|
this.switchBuffer(SERVER_BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (!this.client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(msg) {
|
||||||
|
switch (msg.command) {
|
||||||
|
case irc.RPL_WELCOME:
|
||||||
|
this.setState({ status: REGISTERED });
|
||||||
|
|
||||||
|
if (this.state.connectParams.autojoin.length > 0) {
|
||||||
|
this.client.send({
|
||||||
|
command: "JOIN",
|
||||||
|
params: [this.state.connectParams.autojoin.join(",")],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case irc.RPL_TOPIC:
|
||||||
|
var channel = msg.params[1];
|
||||||
|
var topic = msg.params[2];
|
||||||
|
|
||||||
|
this.setBufferState(channel, (buf) => {
|
||||||
|
return { ...buf, topic };
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case irc.RPL_NAMREPLY:
|
||||||
|
var channel = msg.params[2];
|
||||||
|
var membersList = msg.params.slice(3);
|
||||||
|
|
||||||
|
this.setBufferState(channel, (buf) => {
|
||||||
|
var members = new Map(buf.members);
|
||||||
|
membersList.forEach((s) => {
|
||||||
|
var member = irc.parseMembership(s);
|
||||||
|
members.set(member.nick, member.prefix);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...buf, members };
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case irc.RPL_ENDOFNAMES:
|
||||||
|
break;
|
||||||
|
case "NOTICE":
|
||||||
|
case "PRIVMSG":
|
||||||
|
var target = msg.params[0];
|
||||||
|
if (target == this.client.nick) {
|
||||||
|
target = msg.prefix.name;
|
||||||
|
}
|
||||||
|
this.addMessage(target, msg);
|
||||||
|
break;
|
||||||
|
case "JOIN":
|
||||||
|
var channel = msg.params[0];
|
||||||
|
|
||||||
|
this.createBuffer(channel);
|
||||||
|
this.setBufferState(channel, (buf) => {
|
||||||
|
var members = new Map(buf.members);
|
||||||
|
members.set(msg.prefix.name, null);
|
||||||
|
return { ...buf, members };
|
||||||
|
});
|
||||||
|
if (msg.prefix.name != this.client.nick) {
|
||||||
|
this.addMessage(channel, msg);
|
||||||
|
}
|
||||||
|
if (channel == this.state.connectParams.autojoin[0]) {
|
||||||
|
// TODO: only switch once right after connect
|
||||||
|
this.switchBuffer(channel);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PART":
|
||||||
|
var channel = msg.params[0];
|
||||||
|
|
||||||
|
this.setBufferState(channel, (buf) => {
|
||||||
|
var members = new Map(buf.members);
|
||||||
|
members.delete(msg.prefix.name);
|
||||||
|
return { ...buf, members };
|
||||||
|
});
|
||||||
|
this.addMessage(channel, msg);
|
||||||
|
break;
|
||||||
|
case "NICK":
|
||||||
|
var newNick = msg.params[0];
|
||||||
|
|
||||||
|
var affectedBuffers = [];
|
||||||
|
this.setState((state) => {
|
||||||
|
var buffers = new Map(state.buffers);
|
||||||
|
state.buffers.forEach((buf) => {
|
||||||
|
if (!buf.members.has(msg.prefix.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var members = new Map(buf.members);
|
||||||
|
members.set(newNick, members.get(msg.prefix.name));
|
||||||
|
members.delete(msg.prefix.name);
|
||||||
|
buffers.set(buf.name, { ...buf, members });
|
||||||
|
affectedBuffers.push(buf.name);
|
||||||
|
});
|
||||||
|
return { buffers };
|
||||||
|
});
|
||||||
|
affectedBuffers.forEach((name) => this.addMessage(name, msg));
|
||||||
|
break;
|
||||||
|
case "TOPIC":
|
||||||
|
var channel = msg.params[0];
|
||||||
|
var topic = msg.params[1];
|
||||||
|
|
||||||
|
this.setBufferState((buf) => {
|
||||||
|
return { ...buf, topic };
|
||||||
|
});
|
||||||
|
this.addMessage(channel, msg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.addMessage(SERVER_BUFFER, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnectSubmit(connectParams) {
|
||||||
|
if (localStorage) {
|
||||||
|
if (connectParams.rememberMe) {
|
||||||
|
localStorage.setItem("autoconnect", JSON.stringify(connectParams));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("autoconnect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connect(connectParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommand(s) {
|
||||||
|
var parts = s.split(" ");
|
||||||
|
var cmd = parts[0].toLowerCase().slice(1);
|
||||||
|
var args = parts.slice(1);
|
||||||
|
switch (cmd) {
|
||||||
|
case "quit":
|
||||||
|
if (localStorage) {
|
||||||
|
localStorage.removeItem("autoconnect");
|
||||||
|
}
|
||||||
|
this.disconnect();
|
||||||
|
break;
|
||||||
|
case "join":
|
||||||
|
var channel = args[0];
|
||||||
|
if (!channel) {
|
||||||
|
console.error("Missing channel name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.send({ command: "JOIN", params: [channel] });
|
||||||
|
break;
|
||||||
|
case "part":
|
||||||
|
// TODO: check whether the buffer is a channel with the ISUPPORT token
|
||||||
|
// TODO: part reason
|
||||||
|
if (!this.state.activeBuffer || this.state.activeBuffer == SERVER_BUFFER) {
|
||||||
|
console.error("Not in a channel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var channel = this.state.activeBuffer;
|
||||||
|
this.client.send({ command: "PART", params: [channel] });
|
||||||
|
break;
|
||||||
|
case "msg":
|
||||||
|
var target = args[0];
|
||||||
|
var text = args.slice(1).join(" ");
|
||||||
|
this.client.send({ command: "PRIVMSG", params: [target, text] });
|
||||||
|
break;
|
||||||
|
case "nick":
|
||||||
|
var newNick = args[0];
|
||||||
|
this.client.send({ command: "NICK", params: [newNick] });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unknwon command '" + cmd + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleComposerSubmit(text) {
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.startsWith("//")) {
|
||||||
|
text = text.slice(1);
|
||||||
|
} else if (text.startsWith("/")) {
|
||||||
|
this.executeCommand(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = this.state.activeBuffer;
|
||||||
|
if (!target || target == SERVER_BUFFER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = { command: "PRIVMSG", params: [target, text] };
|
||||||
|
this.client.send(msg);
|
||||||
|
msg.prefix = { name: this.client.nick };
|
||||||
|
this.addMessage(target, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBufferListClick(name) {
|
||||||
|
this.switchBuffer(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (localStorage && localStorage.getItem("autoconnect")) {
|
||||||
|
var connectParams = JSON.parse(localStorage.getItem("autoconnect"));
|
||||||
|
this.connect(connectParams);
|
||||||
|
} else {
|
||||||
|
var params = parseQueryString();
|
||||||
|
|
||||||
|
var serverURL = params.server;
|
||||||
|
if (!serverURL) {
|
||||||
|
var host = window.location.host || "localhost:8080";
|
||||||
|
var proto = "wss:";
|
||||||
|
if (window.location.protocol != "https:") {
|
||||||
|
proto = "ws:";
|
||||||
|
}
|
||||||
|
connectParams.serverURL = proto + "//" + host + "/socket";
|
||||||
|
}
|
||||||
|
|
||||||
|
var autojoin = [];
|
||||||
|
if (params.channels) {
|
||||||
|
autojoin = params.channels.split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState((state) => {
|
||||||
|
return {
|
||||||
|
connectParams: {
|
||||||
|
...state.connectParams,
|
||||||
|
serverURL,
|
||||||
|
autojoin,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.status != REGISTERED) {
|
||||||
|
return html`
|
||||||
|
<section id="connect">
|
||||||
|
<${Connect} params=${this.state.connectParams} disabled=${this.state.status != DISCONNECTED} onSubmit=${this.handleConnectSubmit}/>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeBuffer = null;
|
||||||
|
if (this.state.activeBuffer) {
|
||||||
|
activeBuffer = this.state.buffers.get(this.state.activeBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<section id="sidebar">
|
||||||
|
<${BufferList} buffers=${this.state.buffers} activeBuffer=${this.state.activeBuffer} onBufferClick=${this.handleBufferListClick}/>
|
||||||
|
</section>
|
||||||
|
<section id="buffer">
|
||||||
|
<${Buffer} buffer=${activeBuffer}/>
|
||||||
|
</section>
|
||||||
|
<${Composer} ref=${this.composer} readOnly=${this.state.activeBuffer == SERVER_BUFFER} onSubmit=${this.handleComposerSubmit}/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
29
components/buffer-list.js
Normal file
29
components/buffer-list.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { html, Component } from "/lib/index.js";
|
||||||
|
|
||||||
|
function BufferItem(props) {
|
||||||
|
function handleClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
props.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = props.buffer.name;
|
||||||
|
if (name == "*") {
|
||||||
|
name = "server";
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<li class=${props.active ? "active" : ""}>
|
||||||
|
<a href="#" onClick=${handleClick}>${name}</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BufferList(props) {
|
||||||
|
return html`
|
||||||
|
<ul id="buffer-list">
|
||||||
|
${Array.from(this.props.buffers.values()).map(buf => html`
|
||||||
|
<${BufferItem} buffer=${buf} onClick=${() => props.onBufferClick(buf.name)} active=${props.activeBuffer == buf.name}/>
|
||||||
|
`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
95
components/buffer.js
Normal file
95
components/buffer.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { html, Component } from "/lib/index.js";
|
||||||
|
|
||||||
|
function djb2(s) {
|
||||||
|
var hash = 5381;
|
||||||
|
for (var i = 0; i < s.length; i++) {
|
||||||
|
hash = (hash << 5) + hash + s.charCodeAt(i);
|
||||||
|
hash = hash >>> 0; // convert to uint32
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Nick(props) {
|
||||||
|
function handleClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorIndex = djb2(props.nick) % 16 + 1;
|
||||||
|
return html`
|
||||||
|
<a href="#" class="nick nick-${colorIndex}" onClick=${handleClick}>${props.nick}</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogLine(props) {
|
||||||
|
var msg = props.message;
|
||||||
|
|
||||||
|
var date = new Date();
|
||||||
|
if (msg.tags["time"]) {
|
||||||
|
date = new Date(msg.tags["time"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestamp = date.toLocaleTimeString(undefined, {
|
||||||
|
timeStyle: "short",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
var timestampLink = html`
|
||||||
|
<a href="#" class="timestamp" onClick=${(event) => event.preventDefault()}>${timestamp}</a>
|
||||||
|
`;
|
||||||
|
|
||||||
|
var lineClass = "";
|
||||||
|
var content;
|
||||||
|
switch (msg.command) {
|
||||||
|
case "NOTICE":
|
||||||
|
case "PRIVMSG":
|
||||||
|
var text = msg.params[1];
|
||||||
|
|
||||||
|
var actionPrefix = "\x01ACTION ";
|
||||||
|
if (text.startsWith(actionPrefix) && text.endsWith("\x01")) {
|
||||||
|
var action = text.slice(actionPrefix.length, -1);
|
||||||
|
|
||||||
|
lineClass = "me-tell";
|
||||||
|
content = html`* <${Nick} nick=${msg.prefix.name}/> ${action}`;
|
||||||
|
} else {
|
||||||
|
lineClass = "talk";
|
||||||
|
content = html`${"<"}<${Nick} nick=${msg.prefix.name}/>${">"} ${text}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "JOIN":
|
||||||
|
content = html`
|
||||||
|
<${Nick} nick=${msg.prefix.name}/> has joined
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case "PART":
|
||||||
|
content = html`
|
||||||
|
<${Nick} nick=${msg.prefix.name}/> has left
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case "NICK":
|
||||||
|
var newNick = msg.params[0];
|
||||||
|
content = html`
|
||||||
|
<${Nick} nick=${msg.prefix.name}/> is now known as <${Nick} nick=${newNick}/>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case "TOPIC":
|
||||||
|
var topic = msg.params[1];
|
||||||
|
content = html`
|
||||||
|
<${Nick} nick=${msg.prefix.name}/> changed the topic to: ${topic}
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
content = html`${msg.command} ${msg.params.join(" ")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="logline ${lineClass}">${timestampLink} ${content}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Buffer(props) {
|
||||||
|
if (!props.buffer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.buffer.messages.map((msg) => html`<${LogLine} message=${msg}/>`);
|
||||||
|
}
|
56
components/composer.js
Normal file
56
components/composer.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { html, Component, createRef } from "/lib/index.js";
|
||||||
|
|
||||||
|
export default class Composer extends Component {
|
||||||
|
state = {
|
||||||
|
text: "",
|
||||||
|
};
|
||||||
|
textInput = createRef();
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event) {
|
||||||
|
this.setState({ [event.target.name]: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.props.onSubmit(this.state.text);
|
||||||
|
this.setState({ text: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowKeyDown(event) {
|
||||||
|
if (document.activeElement == document.body && event.key == "/" && !this.state.text) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({ text: "/" }, () => {
|
||||||
|
this.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener("keydown", this.handleWindowKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener("keydown", this.handleWindowKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
document.activeElement.blur(); // in case we're read-only
|
||||||
|
this.textInput.current.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<form id="composer" class="${this.props.readOnly && !this.state.text ? "read-only" : ""}" onChange=${this.handleChange} onSubmit=${this.handleSubmit}>
|
||||||
|
<input type="text" name="text" ref=${this.textInput} value=${this.state.text} placeholder="Type a message"/>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
141
components/connect.js
Normal file
141
components/connect.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { html, Component } from "/lib/index.js";
|
||||||
|
|
||||||
|
export default class Connect extends Component {
|
||||||
|
state = {
|
||||||
|
serverURL: "",
|
||||||
|
serverPass: "",
|
||||||
|
nick: "",
|
||||||
|
password: "",
|
||||||
|
rememberMe: false,
|
||||||
|
username: "",
|
||||||
|
realname: "",
|
||||||
|
autojoin: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
|
||||||
|
if (props.params) {
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
serverURL: props.params.serverURL || "",
|
||||||
|
nick: props.params.nick || "",
|
||||||
|
rememberMe: props.params.rememberMe || false,
|
||||||
|
username: props.params.username || "",
|
||||||
|
realname: props.params.realname || "",
|
||||||
|
autojoin: (props.params.autojoin || []).join(","),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event) {
|
||||||
|
var target = event.target;
|
||||||
|
var value = target.type == "checkbox" ? target.checked : target.value;
|
||||||
|
this.setState({ [target.name]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
serverURL: this.state.serverURL,
|
||||||
|
serverPass: this.state.serverPass,
|
||||||
|
nick: this.state.nick,
|
||||||
|
rememberMe: this.state.rememberMe,
|
||||||
|
username: this.state.username || this.state.nick,
|
||||||
|
realname: this.state.realname || this.state.nick,
|
||||||
|
saslPlain: null,
|
||||||
|
autojoin: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.password) {
|
||||||
|
params.saslPlain = {
|
||||||
|
username: params.username,
|
||||||
|
password: this.state.password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.autojoin.split(",").forEach(function(ch) {
|
||||||
|
ch = ch.trim();
|
||||||
|
if (!ch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
params.autojoin.push(ch);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.onSubmit(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<form onChange=${this.handleChange} onSubmit=${this.handleSubmit}>
|
||||||
|
<h2>Connect to IRC</h2>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Nickname:<br/>
|
||||||
|
<input type="username" name="nick" value=${this.state.nick} disabled=${this.props.disabled} autofocus required/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Password:<br/>
|
||||||
|
<input type="password" name="password" value=${this.state.password} disabled=${this.props.disabled}/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="rememberMe" checked=${this.state.rememberMe} disabled=${this.props.disabled}/>
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Advanced options</summary>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Server URL:<br/>
|
||||||
|
<input type="url" name="serverURL" value=${this.state.serverURL} disabled=${this.props.disabled} required/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Username:<br/>
|
||||||
|
<input type="username" name="username" value=${this.state.username} disabled=${this.props.disabled} placeholder="Same as nickname"/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Real name:<br/>
|
||||||
|
<input type="text" name="realname" value=${this.state.realname} disabled=${this.props.disabled} placeholder="Same as nickname"/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Server password:<br/>
|
||||||
|
<input type="text" name="serverPass" value=${this.state.serverPass} disabled=${this.props.disabled} placeholder="None"/>
|
||||||
|
</label>
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Auto-join channels:<br/>
|
||||||
|
<input type="text" name="autojoin" value=${this.state.autojoin} disabled=${this.props.disabled} placeholder="Comma-separated list of channels"/>
|
||||||
|
</label>
|
||||||
|
<br/>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<button disabled=${this.props.disabled}>Connect</button>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
71
index.html
71
index.html
@ -3,75 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>IRC client</title>
|
<title>IRC client</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="sidebar">
|
|
||||||
<ul id="buffer-list">
|
|
||||||
<!--<li class="active"><a href="#">##soju-playground</a></li>-->
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section id="buffer">
|
|
||||||
<!--<div class="logline">
|
|
||||||
<a href="#" class="timestamp">12:27:42</a>
|
|
||||||
<<a href="#" class="nick">emersion</a>>
|
|
||||||
Hi there!
|
|
||||||
</div>-->
|
|
||||||
</section>
|
|
||||||
<form id="composer">
|
|
||||||
<input type="text" placeholder="Type a message">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<section id="connect">
|
|
||||||
<form>
|
|
||||||
<h2>Connect to IRC</h2>
|
|
||||||
|
|
||||||
<label for="connect-nick">Nickname:</label><br/>
|
|
||||||
<input type="username" name="nick" id="connect-nick" autofocus required/>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<label for="connect-password">Password:</label><br/>
|
|
||||||
<input type="password" name="password" id="connect-password"/>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<input type="checkbox" name="remember-me" id="connect-remember-me"/>
|
|
||||||
<label for="connect-remember-me">Remember me</label>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Advanced options</summary>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<label for="connect-url">Server URL:</label><br/>
|
|
||||||
<input type="url" name="url" id="connect-url"/>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<label for="connect-username">Username:</label><br/>
|
|
||||||
<input type="username" name="username" id="connect-username" placeholder="Same as nickname"/>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<label for="connect-realname">Real name:</label><br/>
|
|
||||||
<input type="text" name="realname" id="connect-realname" placeholder="Same as nickname"/>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<label for="connect-pass">Server password:</label><br/>
|
|
||||||
<input type="text" name="pass" id="connect-pass" placeholder="None"/>
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<label for="connect-autojoin">Auto-join channels:</label><br/>
|
|
||||||
<input type="text" name="autojoin" id="connect-autojoin" placeholder="Comma-separated list of channels"/>
|
|
||||||
<br/>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<button>Connect</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import "./index.js";
|
import { html, render } from "/lib/index.js";
|
||||||
|
import App from "/components/app.js";
|
||||||
|
|
||||||
|
render(html`<${App}/>`, document.body);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
472
index.js
472
index.js
@ -1,472 +0,0 @@
|
|||||||
import * as irc from "./lib/irc.js";
|
|
||||||
import Client from "./lib/client.js";
|
|
||||||
|
|
||||||
var server = {
|
|
||||||
name: "server",
|
|
||||||
username: null,
|
|
||||||
realname: null,
|
|
||||||
nick: null,
|
|
||||||
pass: null,
|
|
||||||
saslPlain: null,
|
|
||||||
autojoin: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
var client = null;
|
|
||||||
|
|
||||||
var buffers = {};
|
|
||||||
var activeBuffer = null;
|
|
||||||
var serverBuffer = null;
|
|
||||||
|
|
||||||
var bufferListElt = document.querySelector("#buffer-list");
|
|
||||||
var bufferElt = document.querySelector("#buffer");
|
|
||||||
var composerElt = document.querySelector("#composer");
|
|
||||||
var composerInputElt = document.querySelector("#composer input");
|
|
||||||
var connectElt = document.querySelector("#connect");
|
|
||||||
var connectFormElt = document.querySelector("#connect form");
|
|
||||||
|
|
||||||
function djb2(s) {
|
|
||||||
var hash = 5381;
|
|
||||||
for (var i = 0; i < s.length; i++) {
|
|
||||||
hash = (hash << 5) + hash + s.charCodeAt(i);
|
|
||||||
hash = hash >>> 0; // convert to uint32
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNickElement(name) {
|
|
||||||
var nick = document.createElement("a");
|
|
||||||
nick.href = "#";
|
|
||||||
nick.className = "nick nick-" + (djb2(name) % 16 + 1);
|
|
||||||
nick.innerText = name;
|
|
||||||
nick.onclick = function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
switchBuffer(createBuffer(name));
|
|
||||||
};
|
|
||||||
return nick;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMessageElement(msg) {
|
|
||||||
var date = new Date();
|
|
||||||
if (msg.tags["time"]) {
|
|
||||||
date = new Date(msg.tags["time"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
line.appendChild(document.createTextNode(" "));
|
|
||||||
|
|
||||||
switch (msg.command) {
|
|
||||||
case "NOTICE":
|
|
||||||
case "PRIVMSG":
|
|
||||||
var text = msg.params[1];
|
|
||||||
|
|
||||||
var actionPrefix = "\x01ACTION ";
|
|
||||||
if (text.startsWith(actionPrefix) && text.endsWith("\x01")) {
|
|
||||||
var action = text.slice(actionPrefix.length, -1);
|
|
||||||
|
|
||||||
line.className += " me-tell";
|
|
||||||
|
|
||||||
line.appendChild(document.createTextNode("* "));
|
|
||||||
line.appendChild(createNickElement(msg.prefix.name));
|
|
||||||
line.appendChild(document.createTextNode(" " + action));
|
|
||||||
} else {
|
|
||||||
line.className += " talk";
|
|
||||||
|
|
||||||
line.appendChild(document.createTextNode("<"));
|
|
||||||
line.appendChild(createNickElement(msg.prefix.name));
|
|
||||||
line.appendChild(document.createTextNode("> "));
|
|
||||||
line.appendChild(document.createTextNode(text));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "JOIN":
|
|
||||||
line.appendChild(createNickElement(msg.prefix.name));
|
|
||||||
line.appendChild(document.createTextNode(" has joined"));
|
|
||||||
break;
|
|
||||||
case "PART":
|
|
||||||
line.appendChild(createNickElement(msg.prefix.name));
|
|
||||||
line.appendChild(document.createTextNode(" has left"));
|
|
||||||
break;
|
|
||||||
case "NICK":
|
|
||||||
var newNick = msg.params[0];
|
|
||||||
line.appendChild(createNickElement(msg.prefix.name));
|
|
||||||
line.appendChild(document.createTextNode(" is now known as "));
|
|
||||||
line.appendChild(createNickElement(newNick));
|
|
||||||
break;
|
|
||||||
case "TOPIC":
|
|
||||||
line.appendChild(createNickElement(msg.prefix.name));
|
|
||||||
line.appendChild(document.createTextNode(" changed the topic to: " + msg.params[1]));
|
|
||||||
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);
|
|
||||||
|
|
||||||
var buf = {
|
|
||||||
name: name,
|
|
||||||
li: li,
|
|
||||||
readOnly: false,
|
|
||||||
topic: null,
|
|
||||||
members: {},
|
|
||||||
messages: [],
|
|
||||||
|
|
||||||
addMessage: function(msg) {
|
|
||||||
if (!msg.tags) {
|
|
||||||
msg.tags = {};
|
|
||||||
}
|
|
||||||
// TODO: set time tag if missing
|
|
||||||
|
|
||||||
buf.messages.push(msg);
|
|
||||||
|
|
||||||
if (activeBuffer === buf) {
|
|
||||||
bufferElt.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");
|
|
||||||
|
|
||||||
bufferElt.innerHTML = "";
|
|
||||||
for (var msg of buf.messages) {
|
|
||||||
bufferElt.appendChild(createMessageElement(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
composerElt.classList.toggle("read-only", buf.readOnly);
|
|
||||||
if (!buf.readOnly) {
|
|
||||||
composerInputElt.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showConnectForm() {
|
|
||||||
setConnectFormDisabled(false);
|
|
||||||
connectElt.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
client = new Client(server);
|
|
||||||
|
|
||||||
client.addEventListener("close", () => {
|
|
||||||
showConnectForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.addEventListener("message", (event) => {
|
|
||||||
var msg = event.detail.message;
|
|
||||||
|
|
||||||
switch (msg.command) {
|
|
||||||
case irc.RPL_WELCOME:
|
|
||||||
connectElt.style.display = "none";
|
|
||||||
|
|
||||||
if (server.autojoin.length > 0) {
|
|
||||||
client.send({
|
|
||||||
command: "JOIN",
|
|
||||||
params: [server.autojoin.join(",")],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case irc.RPL_TOPIC:
|
|
||||||
var channel = msg.params[1];
|
|
||||||
var topic = msg.params[2];
|
|
||||||
|
|
||||||
var buf = buffers[channel];
|
|
||||||
if (!buf) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buf.topic = topic;
|
|
||||||
break;
|
|
||||||
case irc.RPL_NAMREPLY:
|
|
||||||
var channel = msg.params[2];
|
|
||||||
var members = msg.params.slice(3);
|
|
||||||
|
|
||||||
var buf = buffers[channel];
|
|
||||||
if (!buf) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
members.forEach(function(s) {
|
|
||||||
var member = irc.parseMembership(s);
|
|
||||||
buf.members[member.nick] = member.prefix;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case irc.RPL_ENDOFNAMES:
|
|
||||||
break;
|
|
||||||
case "NOTICE":
|
|
||||||
case "PRIVMSG":
|
|
||||||
var target = msg.params[0];
|
|
||||||
if (target == client.nick) {
|
|
||||||
target = msg.prefix.name;
|
|
||||||
}
|
|
||||||
var buf;
|
|
||||||
if (target == "*") {
|
|
||||||
buf = serverBuffer;
|
|
||||||
} else {
|
|
||||||
buf = createBuffer(target);
|
|
||||||
}
|
|
||||||
buf.addMessage(msg);
|
|
||||||
break;
|
|
||||||
case "JOIN":
|
|
||||||
var channel = msg.params[0];
|
|
||||||
var buf = createBuffer(channel);
|
|
||||||
buf.members[msg.prefix.name] = null;
|
|
||||||
if (msg.prefix.name != client.nick) {
|
|
||||||
buf.addMessage(msg);
|
|
||||||
}
|
|
||||||
if (channel == server.autojoin[0]) {
|
|
||||||
// TODO: only switch once right after connect
|
|
||||||
switchBuffer(buf);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "PART":
|
|
||||||
var channel = msg.params[0];
|
|
||||||
var buf = createBuffer(channel);
|
|
||||||
delete buf.members[msg.prefix.name];
|
|
||||||
buf.addMessage(msg);
|
|
||||||
break;
|
|
||||||
case "NICK":
|
|
||||||
var newNick = msg.params[0];
|
|
||||||
for (var name in buffers) {
|
|
||||||
var buf = buffers[name];
|
|
||||||
if (buf.members[msg.prefix.name] !== undefined) {
|
|
||||||
buf.members[newNick] = buf.members[msg.prefix.name];
|
|
||||||
delete buf.members[msg.prefix.name];
|
|
||||||
buf.addMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "TOPIC":
|
|
||||||
var channel = msg.params[0];
|
|
||||||
var topic = msg.params[1];
|
|
||||||
var buf = buffers[channel];
|
|
||||||
if (!buf) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buf.topic = topic;
|
|
||||||
buf.addMessage(msg);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
serverBuffer.addMessage(msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
serverBuffer = createBuffer(server.name);
|
|
||||||
serverBuffer.readOnly = true;
|
|
||||||
switchBuffer(serverBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeCommand(s) {
|
|
||||||
var parts = s.split(" ");
|
|
||||||
var cmd = parts[0].toLowerCase().slice(1);
|
|
||||||
var args = parts.slice(1);
|
|
||||||
switch (cmd) {
|
|
||||||
case "quit":
|
|
||||||
if (localStorage) {
|
|
||||||
localStorage.removeItem("server");
|
|
||||||
}
|
|
||||||
disconnect();
|
|
||||||
break;
|
|
||||||
case "join":
|
|
||||||
var channel = args[0];
|
|
||||||
if (!channel) {
|
|
||||||
console.error("Missing channel name");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client.send({ command: "JOIN", params: [channel] });
|
|
||||||
break;
|
|
||||||
case "part":
|
|
||||||
// TODO: part reason
|
|
||||||
if (!activeBuffer || activeBuffer.readOnly) {
|
|
||||||
console.error("Not in a channel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var channel = activeBuffer.name;
|
|
||||||
client.send({ command: "PART", params: [channel] });
|
|
||||||
break;
|
|
||||||
case "msg":
|
|
||||||
var target = args[0];
|
|
||||||
var text = args.slice(1).join(" ");
|
|
||||||
client.send({ command: "PRIVMSG", params: [target, text] });
|
|
||||||
break;
|
|
||||||
case "nick":
|
|
||||||
var newNick = args[0];
|
|
||||||
client.send({ command: "NICK", params: [newNick] });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error("Unknwon command '" + cmd + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composerElt.onsubmit = function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
var text = composerInputElt.value;
|
|
||||||
composerInputElt.value = "";
|
|
||||||
if (!text) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.startsWith("//")) {
|
|
||||||
text = text.slice(1);
|
|
||||||
} else if (text.startsWith("/")) {
|
|
||||||
executeCommand(text);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activeBuffer || activeBuffer.readOnly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var target = activeBuffer.name;
|
|
||||||
|
|
||||||
var msg = { command: "PRIVMSG", params: [target, text] };
|
|
||||||
client.send(msg);
|
|
||||||
msg.prefix = { name: client.nick };
|
|
||||||
activeBuffer.addMessage(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
function setConnectFormDisabled(disabled) {
|
|
||||||
connectElt.querySelectorAll("input, button").forEach(function(elt) {
|
|
||||||
elt.disabled = disabled;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseQueryString() {
|
|
||||||
var query = window.location.search.substring(1);
|
|
||||||
var params = {};
|
|
||||||
query.split('&').forEach(function(s) {
|
|
||||||
if (!s) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var pair = s.split('=');
|
|
||||||
params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
|
|
||||||
});
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectFormElt.onsubmit = function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
setConnectFormDisabled(true);
|
|
||||||
|
|
||||||
server.url = connectFormElt.elements.url.value;
|
|
||||||
server.nick = connectFormElt.elements.nick.value;
|
|
||||||
server.username = connectFormElt.elements.username.value || server.nick;
|
|
||||||
server.realname = connectFormElt.elements.realname.value || server.nick;
|
|
||||||
server.pass = connectFormElt.elements.pass.value;
|
|
||||||
|
|
||||||
server.saslPlain = null;
|
|
||||||
if (connectFormElt.elements.password.value) {
|
|
||||||
server.saslPlain = {
|
|
||||||
username: server.username,
|
|
||||||
password: connectFormElt.elements.password.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
server.autojoin = [];
|
|
||||||
connectFormElt.elements.autojoin.value.split(",").forEach(function(ch) {
|
|
||||||
ch = ch.trim();
|
|
||||||
if (!ch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
server.autojoin.push(ch);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (localStorage) {
|
|
||||||
if (connectFormElt.elements["remember-me"].checked) {
|
|
||||||
localStorage.setItem("server", JSON.stringify(server));
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem("server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connect();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onkeydown = function(event) {
|
|
||||||
if (activeBuffer && activeBuffer.readOnly && event.key == "/" && document.activeElement != composerInputElt) {
|
|
||||||
// Allow typing commands even in read-only buffers
|
|
||||||
composerElt.classList.remove("read-only");
|
|
||||||
composerInputElt.focus();
|
|
||||||
composerInputElt.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (localStorage && localStorage.getItem("server")) {
|
|
||||||
server = JSON.parse(localStorage.getItem("server"));
|
|
||||||
connectFormElt.elements.url.value = server.url;
|
|
||||||
connectFormElt.elements.nick.value = server.nick;
|
|
||||||
if (server.username != server.nick) {
|
|
||||||
connectFormElt.elements.username.value = server.username;
|
|
||||||
}
|
|
||||||
if (server.realname != server.nick) {
|
|
||||||
connectFormElt.elements.realname.value = server.realname;
|
|
||||||
}
|
|
||||||
connectFormElt.elements["remember-me"].checked = true;
|
|
||||||
setConnectFormDisabled(true);
|
|
||||||
connect();
|
|
||||||
} else {
|
|
||||||
var params = parseQueryString();
|
|
||||||
|
|
||||||
if (params.server) {
|
|
||||||
connectFormElt.elements.url.value = params.server;
|
|
||||||
} else if (!connectFormElt.elements.url.value) {
|
|
||||||
var host = window.location.host || "localhost:8080";
|
|
||||||
var proto = "wss:";
|
|
||||||
if (window.location.protocol != "https:") {
|
|
||||||
proto = "ws:";
|
|
||||||
}
|
|
||||||
connectFormElt.elements.url.value = proto + "//" + host + "/socket";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.channels) {
|
|
||||||
connectFormElt.elements.autojoin.value = params.channels;
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,6 +8,7 @@ export default class Client extends EventTarget {
|
|||||||
ws = null;
|
ws = null;
|
||||||
nick = null;
|
nick = null;
|
||||||
params = {
|
params = {
|
||||||
|
url: null,
|
||||||
username: null,
|
username: null,
|
||||||
realname: null,
|
realname: null,
|
||||||
nick: null,
|
nick: null,
|
||||||
|
5
lib/index.js
Normal file
5
lib/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "/node_modules/preact/dist/preact.module.js";
|
||||||
|
|
||||||
|
import { h } from "/node_modules/preact/dist/preact.module.js";
|
||||||
|
import htm from "/node_modules/htm/dist/htm.module.js";
|
||||||
|
export const html = htm.bind(h);
|
10
package-lock.json
generated
10
package-lock.json
generated
@ -72,6 +72,11 @@
|
|||||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"htm": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/htm/-/htm-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ=="
|
||||||
|
},
|
||||||
"http-proxy": {
|
"http-proxy": {
|
||||||
"version": "1.18.1",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
@ -151,6 +156,11 @@
|
|||||||
"mkdirp": "^0.5.1"
|
"mkdirp": "^0.5.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"preact": {
|
||||||
|
"version": "10.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.4.4.tgz",
|
||||||
|
"integrity": "sha512-EaTJrerceyAPatQ+vfnadoopsMBZAOY7ak9ogVdUi5xbpR8SoHgtLryXnW+4mQOwt21icqoVR1brkU2dq7pEBA=="
|
||||||
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.9.4",
|
"version": "6.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "gamja",
|
"name": "gamja",
|
||||||
|
"dependencies": {
|
||||||
|
"htm": "^3.0.4",
|
||||||
|
"preact": "^10.4.4"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"http-server": "^0.12.3"
|
"http-server": "^0.12.3"
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user