From b449ace4b45ae9c8a055243cdc18ce46d810ddf5 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 18 Jun 2020 14:23:08 +0200 Subject: [PATCH] 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. --- components/app.js | 388 +++++++++++++++++++++++++++++++ components/buffer-list.js | 29 +++ components/buffer.js | 95 ++++++++ components/composer.js | 56 +++++ components/connect.js | 141 ++++++++++++ index.html | 71 +----- index.js | 472 -------------------------------------- lib/client.js | 1 + lib/index.js | 5 + package-lock.json | 10 + package.json | 4 + 11 files changed, 734 insertions(+), 538 deletions(-) create mode 100644 components/app.js create mode 100644 components/buffer-list.js create mode 100644 components/buffer.js create mode 100644 components/composer.js create mode 100644 components/connect.js delete mode 100644 index.js create mode 100644 lib/index.js diff --git a/components/app.js b/components/app.js new file mode 100644 index 0000000..72f1001 --- /dev/null +++ b/components/app.js @@ -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` +
+ <${Connect} params=${this.state.connectParams} disabled=${this.state.status != DISCONNECTED} onSubmit=${this.handleConnectSubmit}/> +
+ `; + } + + var activeBuffer = null; + if (this.state.activeBuffer) { + activeBuffer = this.state.buffers.get(this.state.activeBuffer); + } + + return html` + +
+ <${Buffer} buffer=${activeBuffer}/> +
+ <${Composer} ref=${this.composer} readOnly=${this.state.activeBuffer == SERVER_BUFFER} onSubmit=${this.handleComposerSubmit}/> + `; + } +} diff --git a/components/buffer-list.js b/components/buffer-list.js new file mode 100644 index 0000000..8b455c0 --- /dev/null +++ b/components/buffer-list.js @@ -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` +
  • + ${name} +
  • + `; +} + +export default function BufferList(props) { + return html` + + `; +} diff --git a/components/buffer.js b/components/buffer.js new file mode 100644 index 0000000..e145767 --- /dev/null +++ b/components/buffer.js @@ -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` + ${props.nick} + `; +} + +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` + event.preventDefault()}>${timestamp} + `; + + 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` +
    ${timestampLink} ${content}
    + `; +} + +export default function Buffer(props) { + if (!props.buffer) { + return null; + } + + return props.buffer.messages.map((msg) => html`<${LogLine} message=${msg}/>`); +} diff --git a/components/composer.js b/components/composer.js new file mode 100644 index 0000000..992c135 --- /dev/null +++ b/components/composer.js @@ -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` +
    + +
    + `; + } +} diff --git a/components/connect.js b/components/connect.js new file mode 100644 index 0000000..f8971bf --- /dev/null +++ b/components/connect.js @@ -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` +
    +

    Connect to IRC

    + + +

    + + +

    + + +

    + +
    + Advanced options + +
    + + +

    + + +

    + + +

    + + +

    + + +
    +
    + +
    + + +
    + `; + } +} diff --git a/index.html b/index.html index 04e6df0..c4df574 100644 --- a/index.html +++ b/index.html @@ -3,75 +3,14 @@ IRC client - + - -
    - -
    -
    - -
    - -
    -
    -

    Connect to IRC

    - -
    - -

    - -
    - -

    - - - -

    - -
    - Advanced options - -
    - -
    - -

    - -
    - -

    - -
    - -

    - -
    - -

    - -
    - -
    -
    - -
    - - -
    -
    - diff --git a/index.js b/index.js deleted file mode 100644 index 7d7b07a..0000000 --- a/index.js +++ /dev/null @@ -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; - } -} diff --git a/lib/client.js b/lib/client.js index 3b9835e..3582a8b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -8,6 +8,7 @@ export default class Client extends EventTarget { ws = null; nick = null; params = { + url: null, username: null, realname: null, nick: null, diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..9f5679d --- /dev/null +++ b/lib/index.js @@ -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); diff --git a/package-lock.json b/package-lock.json index 1ead304..9b3a017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,11 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "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": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -151,6 +156,11 @@ "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": { "version": "6.9.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", diff --git a/package.json b/package.json index da4a2a4..c954214 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,9 @@ { "name": "gamja", + "dependencies": { + "htm": "^3.0.4", + "preact": "^10.4.4" + }, "devDependencies": { "http-server": "^0.12.3" },