1 Commits

Author SHA1 Message Date
Simon Ser
488e11eddc eslint: add extra rules 2024-11-16 12:59:42 +01:00
22 changed files with 1869 additions and 1507 deletions

View File

@@ -1,4 +1,5 @@
image: alpine/latest # TODO switch back to alpine/latest once the "npm install" deadlock is fixed
image: alpine/edge
packages: packages:
- npm - npm
- rsync - rsync
@@ -6,16 +7,13 @@ sources:
- https://codeberg.org/emersion/gamja.git - https://codeberg.org/emersion/gamja.git
secrets: secrets:
- 7a146c8e-aeb4-46e7-99bf-05af7486bbe9 # deploy SSH key - 7a146c8e-aeb4-46e7-99bf-05af7486bbe9 # deploy SSH key
artifacts:
- gamja/gamja.tar.gz
tasks: tasks:
- setup: | - setup: |
cd gamja cd gamja
npm clean-install --include=dev npm install --include=dev
- build: | - build: |
cd gamja cd gamja
npm run build npm run build
tar -czf gamja.tar.gz -C dist .
- lint: | - lint: |
cd gamja cd gamja
npm run -- lint --max-warnings 0 npm run -- lint --max-warnings 0

View File

@@ -1,37 +0,0 @@
name: Bun Package
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install deps
run: bun install
- name: Build gamja
run: bun build ./index.html --outdir ./dist --minify
- name: Generar fecha
run: echo "BUILD_DATE=$(date +'%Y-%m-%d_%H-%M')" >> $GITHUB_ENV
- name: Upload dist
uses: actions/upload-artifact@v3
with:
name: gamja-dist-${{ env.BUILD_DATE }}
path: dist

View File

@@ -2,7 +2,7 @@
A simple IRC web client. A simple IRC web client.
<img src="https://fs.emersion.fr/protected/img/gamja/main.png" alt="Screenshot" width="800"> ![screenshot](https://l.sr.ht/7Npm.png)
## Usage ## Usage

View File

@@ -124,7 +124,7 @@ const commands = [
if (args.length) { if (args.length) {
params.push(args.join(" ")); params.push(args.join(" "));
} }
getActiveClient(app).send({ command: "AWAY", params }); getActiveClient(app).send({command: "AWAY", params});
}, },
}, },
ban, ban,
@@ -190,10 +190,9 @@ const commands = [
throw new Error("Missing nick"); throw new Error("Missing nick");
} }
let activeChannel = getActiveChannel(app); let activeChannel = getActiveChannel(app);
getActiveClient(app).send({ getActiveClient(app).send({ command: "INVITE", params: [
command: "INVITE", nick, activeChannel,
params: [nick, activeChannel], ]});
});
}, },
}, },
{ ...join, name: "j" }, { ...join, name: "j" },

View File

@@ -220,7 +220,6 @@ export default class App extends Component {
this.handleAddNetworkClick = this.handleAddNetworkClick.bind(this); this.handleAddNetworkClick = this.handleAddNetworkClick.bind(this);
this.handleNetworkSubmit = this.handleNetworkSubmit.bind(this); this.handleNetworkSubmit = this.handleNetworkSubmit.bind(this);
this.handleNetworkRemove = this.handleNetworkRemove.bind(this); this.handleNetworkRemove = this.handleNetworkRemove.bind(this);
this.showError = this.showError.bind(this);
this.handleDismissError = this.handleDismissError.bind(this); this.handleDismissError = this.handleDismissError.bind(this);
this.handleAuthSubmit = this.handleAuthSubmit.bind(this); this.handleAuthSubmit = this.handleAuthSubmit.bind(this);
this.handleRegisterSubmit = this.handleRegisterSubmit.bind(this); this.handleRegisterSubmit = this.handleRegisterSubmit.bind(this);
@@ -324,8 +323,6 @@ export default class App extends Component {
} }
if (queryParams.debug === "1") { if (queryParams.debug === "1") {
this.debug = true; this.debug = true;
} else if (queryParams.debug === "0") {
this.debug = false;
} }
if (window.location.hash) { if (window.location.hash) {
@@ -668,6 +665,12 @@ export default class App extends Component {
} }
} }
addChatMessage(serverID, bufName, msg) {
this.prepareChatMessage(serverID, msg);
let bufID = { server: serverID, name: bufName };
this.setState((state) => State.addMessage(state, msg, bufID));
}
handleChatMessage(serverID, bufName, msg) { handleChatMessage(serverID, bufName, msg) {
let client = this.clients.get(serverID); let client = this.clients.get(serverID);
@@ -757,7 +760,7 @@ export default class App extends Component {
// Open a new buffer if the message doesn't come from me or is a // Open a new buffer if the message doesn't come from me or is a
// self-message // self-message
if ((!client.isMyNick(msg.prefix.name) || client.isMyNick(bufName)) && (msg.command !== "PART" && msg.command !== "QUIT" && msg.command !== irc.RPL_MONONLINE && msg.command !== irc.RPL_MONOFFLINE)) { if ((!client.isMyNick(msg.prefix.name) || client.isMyNick(bufName)) && (msg.command !== "PART" && msg.comand !== "QUIT" && msg.command !== irc.RPL_MONONLINE && msg.command !== irc.RPL_MONOFFLINE)) {
this.createBuffer(serverID, bufName); this.createBuffer(serverID, bufName);
} }
@@ -1070,7 +1073,6 @@ export default class App extends Component {
case "ACK": case "ACK":
case "BOUNCER": case "BOUNCER":
case "MARKREAD": case "MARKREAD":
case "REDACT":
// Ignore these // Ignore these
return []; return [];
default: default:
@@ -1795,12 +1797,7 @@ export default class App extends Component {
} }
for (let msg of result.messages) { for (let msg of result.messages) {
this.prepareChatMessage(buf.server, msg); this.addChatMessage(buf.server, buf.name, msg);
let destBuffers = this.routeMessage(buf.server, msg);
for (let bufName of destBuffers) {
let bufID = { server: buf.server, name: bufName };
this.setState((state) => State.addMessage(state, msg, bufID));
}
} }
} }
@@ -2006,9 +2003,7 @@ export default class App extends Component {
this.lastFocusPingDate = now; this.lastFocusPingDate = now;
for (let client of this.clients.values()) { for (let client of this.clients.values()) {
if (client.status === Client.Status.REGISTERED) { client.send({ command: "PING", params: ["gamja"] });
client.send({ command: "PING", params: ["gamja"] });
}
} }
} }
@@ -2072,7 +2067,7 @@ export default class App extends Component {
} }
bufferHeader = html` bufferHeader = html`
<section id="buffer-header" role="banner"> <section id="buffer-header">
<${BufferHeader} <${BufferHeader}
buffer=${activeBuffer} buffer=${activeBuffer}
server=${activeServer} server=${activeServer}
@@ -2094,10 +2089,8 @@ export default class App extends Component {
if (activeBuffer && activeBuffer.type === BufferType.CHANNEL) { if (activeBuffer && activeBuffer.type === BufferType.CHANNEL) {
memberList = html` memberList = html`
<section <section
id="member-list" id="member-list"
class=${this.state.openPanels.memberList ? "expand" : ""} class=${this.state.openPanels.memberList ? "expand" : ""}
role="complementary"
aria-label="Members list"
> >
<button <button
class="expander" class="expander"
@@ -2224,7 +2217,7 @@ export default class App extends Component {
let error = null; let error = null;
if (this.state.error) { if (this.state.error) {
error = html` error = html`
<div id="error-msg" role="alert"> <div id="error-msg">
${this.state.error} ${this.state.error}
${" "} ${" "}
<button onClick=${this.handleDismissError}>×</button> <button onClick=${this.handleDismissError}>×</button>
@@ -2248,8 +2241,8 @@ export default class App extends Component {
let app = html` let app = html`
<section <section
id="buffer-list" id="buffer-list"
class=${this.state.openPanels.bufferList ? "expand" : ""} class=${this.state.openPanels.bufferList ? "expand" : ""}
> >
<${BufferList} <${BufferList}
buffers=${this.state.buffers} buffers=${this.state.buffers}
@@ -2274,7 +2267,7 @@ export default class App extends Component {
scrollKey=${this.state.activeBuffer} scrollKey=${this.state.activeBuffer}
onScrollTop=${this.handleBufferScrollTop} onScrollTop=${this.handleBufferScrollTop}
> >
<section id="buffer" ref=${this.buffer} tabindex="-1" role="log"> <section id="buffer" ref=${this.buffer} tabindex="-1">
<${Buffer} <${Buffer}
buffer=${activeBuffer} buffer=${activeBuffer}
server=${activeServer} server=${activeServer}
@@ -2293,7 +2286,6 @@ export default class App extends Component {
client=${activeClient} client=${activeClient}
readOnly=${composerReadOnly} readOnly=${composerReadOnly}
onSubmit=${this.handleComposerSubmit} onSubmit=${this.handleComposerSubmit}
onError=${this.showError}
autocomplete=${this.autocomplete} autocomplete=${this.autocomplete}
commandOnly=${commandOnly} commandOnly=${commandOnly}
maxLen=${privmsgMaxLen} maxLen=${privmsgMaxLen}

View File

@@ -47,7 +47,7 @@ function BufferItem(props) {
} }
return html` return html`
<li class="${classes.join(" ")}" role="tab" aria-selected="${props.active}"> <li class="${classes.join(" ")}">
<a <a
href=${getBufferURL(props.buffer)} href=${getBufferURL(props.buffer)}
title=${title} title=${title}
@@ -80,9 +80,5 @@ export default function BufferList(props) {
`; `;
}); });
return html` return html`<ul>${items}</ul>`;
<ul role="tablist" aria-label="Buffer list">
${items}
</ul>
`;
} }

View File

@@ -43,7 +43,7 @@ function _Timestamp({ date, url, showSeconds }) {
if (showSeconds) { if (showSeconds) {
timestamp += ":--"; timestamp += ":--";
} }
return html`<span class="timestamp" aria-hidden="true">${timestamp}</span>`; return html`<spam class="timestamp">${timestamp}</span>`;
} }
let hh = date.getHours().toString().padStart(2, "0"); let hh = date.getHours().toString().padStart(2, "0");
@@ -94,7 +94,7 @@ function canFoldMessage(msg) {
class LogLine extends Component { class LogLine extends Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return this.props.message !== nextProps.message || this.props.redacted !== nextProps.redacted; return this.props.message !== nextProps.message;
} }
render() { render() {
@@ -143,24 +143,12 @@ class LogLine extends Component {
`; `;
} }
} else { } else {
lineClass = "talk";
let prefix = "<", suffix = ">"; let prefix = "<", suffix = ">";
if (msg.command === "NOTICE") { if (msg.command === "NOTICE") {
lineClass += " notice";
prefix = suffix = "-"; prefix = suffix = "-";
} }
if (this.props.redacted) { content = html`${prefix}${createNick(msg.prefix.name)}${suffix} ${linkify(stripANSI(text), onChannelClick)}`;
content = html`<i>This message has been deleted.</i>`;
} else {
content = html`${linkify(stripANSI(text), onChannelClick)}`;
lineClass += " talk";
}
content = html`
<span class="nick-caret" aria-hidden="true">${prefix}</span>
${createNick(msg.prefix.name)}
<span class="nick-caret" aria-hidden="true">${suffix}</span>
${" "}
${content}
`;
} }
let allowedPrefixes = server.statusMsg; let allowedPrefixes = server.statusMsg;
@@ -286,15 +274,9 @@ class LogLine extends Component {
break; break;
case "TOPIC": case "TOPIC":
let topic = msg.params[1]; let topic = msg.params[1];
if (topic) { content = html`
content = html` ${createNick(msg.prefix.name)} changed the topic to: ${linkify(stripANSI(topic), onChannelClick)}
${createNick(msg.prefix.name)} changed the topic to: ${linkify(stripANSI(topic), onChannelClick)} `;
`;
} else {
content = html`
${createNick(msg.prefix.name)} cleared the topic
`;
}
break; break;
case "INVITE": case "INVITE":
invitee = msg.params[0]; invitee = msg.params[0];
@@ -386,7 +368,7 @@ class LogLine extends Component {
} }
return html` return html`
<div class="logline ${lineClass}" data-key=${msg.key} role="listitem"> <div class="logline ${lineClass}" data-key=${msg.key}>
<${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/> <${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/>
${" "} ${" "}
${content} ${content}
@@ -511,7 +493,7 @@ class FoldGroup extends Component {
} }
return html` return html`
<div class="logline" data-key=${msgs[0].key} role="listitem"> <div class="logline" data-key=${msgs[0].key}>
${timestamp} ${timestamp}
${" "} ${" "}
${content} ${content}
@@ -564,7 +546,7 @@ class NotificationNagger extends Component {
} }
return html` return html`
<div class="logline nag" role="listitem"> <div class="logline">
<${Timestamp}/> <${Timestamp}/>
${" "} ${" "}
<a href="#" onClick=${this.handleClick}>Turn on desktop notifications</a> to get notified about new messages <a href="#" onClick=${this.handleClick}>Turn on desktop notifications</a> to get notified about new messages
@@ -605,7 +587,7 @@ class ProtocolHandlerNagger extends Component {
} }
let name = this.props.bouncerName || "this bouncer"; let name = this.props.bouncerName || "this bouncer";
return html` return html`
<div class="logline nag" role="listitem"> <div class="logline">
<${Timestamp}/> <${Timestamp}/>
${" "} ${" "}
<a href="#" onClick=${this.handleClick}>Register our protocol handler</a> to open IRC links with ${name} <a href="#" onClick=${this.handleClick}>Register our protocol handler</a> to open IRC links with ${name}
@@ -643,7 +625,7 @@ function AccountNagger({ server, onAuthClick, onRegisterClick }) {
} }
return html` return html`
<div class="logline nag" role="listitem"> <div class="logline">
<${Timestamp}/> ${msg} <${Timestamp}/> ${msg}
</div> </div>
`; `;
@@ -662,7 +644,7 @@ class DateSeparator extends Component {
let date = this.props.date; let date = this.props.date;
let text = date.toLocaleDateString([], { year: "numeric", month: "2-digit", day: "2-digit" }); let text = date.toLocaleDateString([], { year: "numeric", month: "2-digit", day: "2-digit" });
return html` return html`
<div class="separator date-separator" role="separator"> <div class="separator date-separator">
${text} ${text}
</div> </div>
`; `;
@@ -670,7 +652,7 @@ class DateSeparator extends Component {
} }
function UnreadSeparator(props) { function UnreadSeparator(props) {
return html`<div class="separator unread-separator" role="separator">New messages</div>`; return html`<div class="separator unread-separator">New messages</div>`;
} }
function sameDate(d1, d2) { function sameDate(d1, d2) {
@@ -721,7 +703,6 @@ export default class Buffer extends Component {
message=${msg} message=${msg}
buffer=${buf} buffer=${buf}
server=${server} server=${server}
redacted=${buf.redacted.has(msg.tags.msgid)}
onChannelClick=${onChannelClick} onChannelClick=${onChannelClick}
onNickClick=${onNickClick} onNickClick=${onNickClick}
onVerifyClick=${onVerifyClick} onVerifyClick=${onVerifyClick}
@@ -827,7 +808,7 @@ export default class Buffer extends Component {
if (sep.length > 0) { if (sep.length > 0) {
children.push(createFoldGroup(foldMessages)); children.push(createFoldGroup(foldMessages));
children.push(...sep); children.push(sep);
foldMessages = []; foldMessages = [];
} }
@@ -847,7 +828,7 @@ export default class Buffer extends Component {
children.push(createFoldGroup(foldMessages)); children.push(createFoldGroup(foldMessages));
return html` return html`
<div class="logline-list" role="list"> <div class="logline-list">
${children} ${children}
</div> </div>
`; `;

View File

@@ -185,13 +185,7 @@ export default class Composer extends Component {
promises.push(this.uploadFile(file)); promises.push(this.uploadFile(file));
} }
let urls; let urls = await Promise.all(promises);
try {
urls = await Promise.all(promises);
} catch (err) {
this.props.onError(new Error("Failed to upload files", { cause: err }));
return;
}
this.setState((state) => { this.setState((state) => {
if (state.text) { if (state.text) {

View File

@@ -47,11 +47,11 @@ export default class Dialog extends Component {
render() { render() {
return html` return html`
<div class="dialog" onClick=${this.handleBackdropClick} role="dialog" aria-modal="true"> <div class="dialog" onClick=${this.handleBackdropClick}>
<div class="dialog-body" ref=${this.body}> <div class="dialog-body" ref=${this.body}>
<div class="dialog-header"> <div class="dialog-header">
<h2>${this.props.title}</h2> <h2>${this.props.title}</h2>
<button class="dialog-close" onClick=${this.handleCloseClick} title="Close">×</button> <button class="dialog-close" onClick=${this.handleCloseClick}>×</button>
</div> </div>
${this.props.children} ${this.props.children}
</div> </div>

View File

@@ -42,8 +42,8 @@ function KeyBindingsHelp() {
} }
function CommandsHelp() { function CommandsHelp() {
let l = [...commands.keys()].map((name) => { let l = Object.keys(commands).map((name) => {
let cmd = commands.get(name); let cmd = commands[name];
let usage = [html`<strong>/${name}</strong>`]; let usage = [html`<strong>/${name}</strong>`];
if (cmd.usage) { if (cmd.usage) {

View File

@@ -65,7 +65,7 @@ export default class SwitcherForm extends Component {
this.handleInput = this.handleInput.bind(this); this.handleInput = this.handleInput.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this);
} }
getSuggestions() { getSuggestions() {
@@ -106,7 +106,7 @@ export default class SwitcherForm extends Component {
this.props.onSubmit(this.getSuggestions()[this.state.selected]); this.props.onSubmit(this.getSuggestions()[this.state.selected]);
} }
handleKeyDown(event) { handleKeyUp(event) {
switch (event.key) { switch (event.key) {
case "ArrowUp": case "ArrowUp":
event.stopPropagation(); event.stopPropagation();
@@ -152,7 +152,7 @@ export default class SwitcherForm extends Component {
<form <form
onInput=${this.handleInput} onInput=${this.handleInput}
onSubmit=${this.handleSubmit} onSubmit=${this.handleSubmit}
onKeyDown=${this.handleKeyDown} onKeyUp=${this.handleKeyUp}
> >
<input <input
type="search" type="search"

View File

@@ -63,8 +63,7 @@ if (remoteHost) {
ws.close(); ws.close();
}); });
client.on("error", (err) => { client.on("error", () => {
console.log(err);
ws.close(WS_BAD_GATEWAY); ws.close(WS_BAD_GATEWAY);
}); });
}); });

View File

@@ -7,7 +7,7 @@ gamja settings can be overridden using URL query parameters:
replaced with a randomly generated value) replaced with a randomly generated value)
- `channels`: comma-separated list of channels to join (`#` needs to be escaped) - `channels`: comma-separated list of channels to join (`#` needs to be escaped)
- `open`: [IRC URL] to open - `open`: [IRC URL] to open
- `debug`: enable debug logs if set to `1`, disable debug logs if set to `0` - `debug`: if set to 1, debug mode is enabled
Alternatively, the channels can be set with the URL fragment (ie, by just Alternatively, the channels can be set with the URL fragment (ie, by just
appending the channel name to the gamja URL). appending the channel name to the gamja URL).

View File

@@ -1,6 +1,6 @@
import globals from "globals"; import globals from "globals";
import js from "@eslint/js"; import js from "@eslint/js";
import stylistic from "@stylistic/eslint-plugin"; import stylisticJs from "@stylistic/eslint-plugin-js";
export default [ export default [
{ {
@@ -14,7 +14,7 @@ export default [
"process": "readonly", "process": "readonly",
}, },
}, },
plugins: { "@stylistic": stylistic }, plugins: { "@stylistic/js": stylisticJs },
rules: { rules: {
"no-case-declarations": "off", "no-case-declarations": "off",
"no-unused-vars": ["error", { "no-unused-vars": ["error", {
@@ -23,34 +23,19 @@ export default [
destructuredArrayIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_",
}], }],
"no-var": "error", "no-var": "error",
"no-eval": "error",
"no-implied-eval": "error",
"eqeqeq": "error", "eqeqeq": "error",
"no-invalid-this": "error", "no-invalid-this": "error",
"no-extend-native": "error",
"prefer-arrow-callback": "error", "prefer-arrow-callback": "error",
"no-implicit-globals": "error", "require-atomic-updates": "error",
"no-throw-literal": "error",
"no-implicit-coercion": "warn", "no-implicit-coercion": "warn",
"object-shorthand": "warn", "object-shorthand": "warn",
"curly": "warn", //"sort-imports": ["warn", { ignoreMemberSort: true, allowSeparatedGroups: true }],
"camelcase": "warn", //"func-style": ["warn", "declaration"],
"@stylistic/indent": ["warn", "tab", { SwitchCase: 0 }], "@stylistic/js/indent": ["warn", "tab"],
"@stylistic/quotes": ["warn", "double"], "@stylistic/js/quotes": ["warn", "double"],
"@stylistic/semi": "warn", "@stylistic/js/semi": "warn",
"@stylistic/brace-style": ["warn", "1tbs"], "@stylistic/js/comma-dangle": ["warn", "always-multiline"],
"@stylistic/comma-dangle": ["warn", "always-multiline"], "@stylistic/js/arrow-parens": "warn",
"@stylistic/comma-spacing": "warn",
"@stylistic/arrow-parens": "warn",
"@stylistic/arrow-spacing": "warn",
"@stylistic/block-spacing": "warn",
"@stylistic/object-curly-spacing": ["warn", "always"],
"@stylistic/object-curly-newline": ["warn", {
multiline: true,
consistent: true,
}],
"@stylistic/array-bracket-spacing": ["warn", "never"],
"@stylistic/array-bracket-newline": ["warn", "consistent"],
}, },
}, },
]; ];

View File

@@ -6,7 +6,7 @@
<title>gamja IRC client</title> <title>gamja IRC client</title>
<link rel="stylesheet" href="./style.css"> <link rel="stylesheet" href="./style.css">
<script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
<body> <body>

View File

@@ -21,7 +21,6 @@ const permanentCaps = [
"draft/account-registration", "draft/account-registration",
"draft/chathistory", "draft/chathistory",
"draft/extended-monitor", "draft/extended-monitor",
"draft/message-redaction",
"draft/read-marker", "draft/read-marker",
"soju.im/bouncer-networks", "soju.im/bouncer-networks",
@@ -32,33 +31,9 @@ const RECONNECT_MAX_DELAY_MSEC = 10 * 60 * 1000; // 10min
// WebSocket status codes // WebSocket status codes
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1 // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
const WEBSOCKET_CLOSE_CODES = { const NORMAL_CLOSURE = 1000;
NORMAL_CLOSURE: 1000, const GOING_AWAY = 1001;
GOING_AWAY: 1001, const UNSUPPORTED_DATA = 1003;
PROTOCOL_ERROR: 1002,
UNSUPPORTED_DATA: 1003,
NO_STATUS_CODE: 1005,
ABNORMAL_CLOSURE: 1006,
INVALID_FRAME_PAYLOAD_DATA: 1007,
POLICY_VIOLATION: 1008,
MESSAGE_TOO_BIG: 1009,
MISSING_MANDATORY_EXT: 1010,
INTERNAL_SERVER_ERROR: 1011,
TLS_HANDSHAKE_FAILED: 1015,
};
const WEBSOCKET_CLOSE_CODE_NAMES = {
[WEBSOCKET_CLOSE_CODES.GOING_AWAY]: "going away",
[WEBSOCKET_CLOSE_CODES.PROTOCOL_ERROR]: "protocol error",
[WEBSOCKET_CLOSE_CODES.UNSUPPORTED_DATA]: "unsupported data",
[WEBSOCKET_CLOSE_CODES.NO_STATUS_CODE]: "no status code received",
[WEBSOCKET_CLOSE_CODES.ABNORMAL_CLOSURE]: "abnormal closure",
[WEBSOCKET_CLOSE_CODES.INVALID_FRAME_PAYLOAD_DATA]: "invalid frame payload data",
[WEBSOCKET_CLOSE_CODES.POLICY_VIOLATION]: "policy violation",
[WEBSOCKET_CLOSE_CODES.MESSAGE_TOO_BIG]: "message too big",
[WEBSOCKET_CLOSE_CODES.MISSING_MANDATORY_EXT]: "missing mandatory extension",
[WEBSOCKET_CLOSE_CODES.INTERNAL_SERVER_ERROR]: "internal server error",
[WEBSOCKET_CLOSE_CODES.TLS_HANDSHAKE_FAILED]: "TLS handshake failed",
};
// See https://github.com/quakenet/snircd/blob/master/doc/readme.who // See https://github.com/quakenet/snircd/blob/master/doc/readme.who
// Sorted by order of appearance in RPL_WHOSPCRPL // Sorted by order of appearance in RPL_WHOSPCRPL
@@ -93,18 +68,6 @@ class IRCError extends Error {
} }
} }
class WebSocketError extends Error {
constructor(code) {
let text = "Connection error";
let name = WEBSOCKET_CLOSE_CODE_NAMES[code];
if (name) {
text += " (" + name + ")";
}
super(text);
}
}
/** /**
* Implements a simple exponential backoff. * Implements a simple exponential backoff.
*/ */
@@ -225,8 +188,8 @@ export default class Client extends EventTarget {
this.ws.addEventListener("close", (event) => { this.ws.addEventListener("close", (event) => {
console.log("Connection closed (code: " + event.code + ")"); console.log("Connection closed (code: " + event.code + ")");
if (event.code !== WEBSOCKET_CLOSE_CODES.NORMAL_CLOSURE && event.code !== WEBSOCKET_CLOSE_CODES.GOING_AWAY) { if (event.code !== NORMAL_CLOSURE && event.code !== GOING_AWAY) {
this.dispatchError(new WebSocketError(event.code)); this.dispatchError(new Error("Connection error"));
} }
this.ws = null; this.ws = null;
@@ -273,7 +236,7 @@ export default class Client extends EventTarget {
this.setPingInterval(0); this.setPingInterval(0);
if (this.ws) { if (this.ws) {
this.ws.close(WEBSOCKET_CLOSE_CODES.NORMAL_CLOSURE); this.ws.close(NORMAL_CLOSURE);
} }
} }
@@ -333,7 +296,7 @@ export default class Client extends EventTarget {
handleMessage(event) { handleMessage(event) {
if (typeof event.data !== "string") { if (typeof event.data !== "string") {
console.error("Received unsupported data type:", event.data); console.error("Received unsupported data type:", event.data);
this.ws.close(WEBSOCKET_CLOSE_CODES.UNSUPPORTED_DATA); this.ws.close(UNSUPPORTED_DATA);
return; return;
} }

View File

@@ -4,5 +4,5 @@ import { h } from "../node_modules/preact/dist/preact.module.js";
import htm from "../node_modules/htm/dist/htm.module.js"; import htm from "../node_modules/htm/dist/htm.module.js";
export const html = htm.bind(h); export const html = htm.bind(h);
import * as linkifyjs from "../node_modules/linkifyjs/dist/linkify.mjs"; import * as linkifyjs from "../node_modules/linkifyjs/dist/linkify.es.js";
export { linkifyjs }; export { linkifyjs };

View File

@@ -43,9 +43,9 @@ export function redirectAuthorize({ serverMetadata, clientId, redirectUri, scope
// TODO: use the state param to prevent cross-site request // TODO: use the state param to prevent cross-site request
// forgery // forgery
let params = { let params = {
"response_type": "code", response_type: "code",
"client_id": clientId, client_id: clientId,
"redirect_uri": redirectUri, redirect_uri: redirectUri,
}; };
if (scope) { if (scope) {
params.scope = scope; params.scope = scope;
@@ -66,12 +66,12 @@ function buildPostHeaders(clientId, clientSecret) {
export async function exchangeCode({ serverMetadata, redirectUri, code, clientId, clientSecret }) { export async function exchangeCode({ serverMetadata, redirectUri, code, clientId, clientSecret }) {
let data = { let data = {
"grant_type": "authorization_code", grant_type: "authorization_code",
code, code,
"redirect_uri": redirectUri, redirect_uri: redirectUri,
}; };
if (!clientSecret) { if (!clientSecret) {
data["client_id"] = clientId; data.client_id = clientId;
} }
let resp = await fetch(serverMetadata.token_endpoint, { let resp = await fetch(serverMetadata.token_endpoint, {

3033
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,15 @@
"dependencies": { "dependencies": {
"htm": "^3.0.4", "htm": "^3.0.4",
"linkifyjs": "^4.1.3", "linkifyjs": "^4.1.3",
"preact": "^10.17.1" "preact": "10.17.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.11.1", "@eslint/js": "^9.11.1",
"@parcel/packager-raw-url": "^2.0.0", "@parcel/packager-raw-url": "^2.0.0",
"@parcel/transformer-webmanifest": "^2.0.0", "@parcel/transformer-webmanifest": "^2.0.0",
"@stylistic/eslint-plugin": "^5.1.0", "@stylistic/eslint-plugin-js": "^2.8.0",
"eslint": "^9.11.1", "eslint": "^9.11.1",
"globals": "^17.0.0", "globals": "^15.9.0",
"node-static": "^0.7.11", "node-static": "^0.7.11",
"parcel": "^2.0.0", "parcel": "^2.0.0",
"split": "^1.0.1", "split": "^1.0.1",

View File

@@ -153,24 +153,10 @@ function trimStartCharacter(s, c) {
return s.substring(i); return s.substring(i);
} }
function getBouncerNetworkNameFromBuffer(state, buffer) {
let server = state.servers.get(buffer.server);
let network = state.bouncerNetworks.get(server.bouncerNetID);
if (!network) {
return null;
}
return getServerName(server, network);
}
/* Returns 1 if a should appear after b, -1 if a should appear before b, or /* Returns 1 if a should appear after b, -1 if a should appear before b, or
* 0 otherwise. */ * 0 otherwise. */
function compareBuffers(state, a, b) { function compareBuffers(a, b) {
if (a.server !== b.server) { if (a.server !== b.server) {
let aServerName = getBouncerNetworkNameFromBuffer(state, a);
let bServerName = getBouncerNetworkNameFromBuffer(state, b);
if (aServerName && bServerName && aServerName !== bServerName) {
return aServerName.localeCompare(bServerName);
}
return a.server > b.server ? 1 : -1; return a.server > b.server ? 1 : -1;
} }
if (isServerBuffer(a) !== isServerBuffer(b)) { if (isServerBuffer(a) !== isServerBuffer(b)) {
@@ -231,7 +217,7 @@ function insertMessage(list, msg) {
} }
console.assert(insertBefore >= 0, ""); console.assert(insertBefore >= 0, "");
list = [...list]; list = [ ...list ];
list.splice(insertBefore, 0, msg); list.splice(insertBefore, 0, msg);
return list; return list;
} }
@@ -375,11 +361,10 @@ export const State = {
hasInitialWho: false, // if channel hasInitialWho: false, // if channel
members: new irc.CaseMapMap(null, client.cm), // if channel members: new irc.CaseMapMap(null, client.cm), // if channel
messages: [], messages: [],
redacted: new Set(),
unread: Unread.NONE, unread: Unread.NONE,
prevReadReceipt: null, prevReadReceipt: null,
}); });
bufferList = bufferList.sort((a, b) => compareBuffers(state, a, b)); bufferList = bufferList.sort(compareBuffers);
let buffers = new Map(bufferList.map((buf) => [buf.id, buf])); let buffers = new Map(bufferList.map((buf) => [buf.id, buf]));
return [id, { buffers }]; return [id, { buffers }];
}, },
@@ -611,12 +596,11 @@ export const State = {
if (buf.server !== serverID) { if (buf.server !== serverID) {
return; return;
} }
let membership = members.get(msg.prefix.name); if (!buf.members.has(msg.prefix.name)) {
if (membership === undefined) {
return; return;
} }
let members = new irc.CaseMapMap(buf.members); let members = new irc.CaseMapMap(buf.members);
members.set(newNick, membership); members.set(newNick, members.get(msg.prefix.name));
members.delete(msg.prefix.name); members.delete(msg.prefix.name);
buffers.set(buf.id, { ...buf, members }); buffers.set(buf.id, { ...buf, members });
}); });
@@ -671,28 +655,16 @@ export const State = {
let members = new irc.CaseMapMap(buf.members); let members = new irc.CaseMapMap(buf.members);
irc.forEachChannelModeUpdate(msg, client.isupport, (mode, add, arg) => { irc.forEachChannelModeUpdate(msg, client.isupport, (mode, add, arg) => {
if (!prefixByMode.has(mode)) { if (prefixByMode.has(mode)) {
return; let nick = arg;
let membership = members.get(nick);
let letter = prefixByMode.get(mode);
members.set(nick, updateMembership(membership, letter, add, client));
} }
let nick = arg;
let membership = members.get(nick);
if (membership === undefined) {
return;
}
let letter = prefixByMode.get(mode);
members.set(nick, updateMembership(membership, letter, add, client));
}); });
return { members }; return { members };
}); });
case "REDACT":
target = msg.params[0];
if (client.isMyNick(target)) {
target = msg.prefix.name;
}
return updateBuffer(target, (buf) => {
return { redacted: new Set(buf.redacted).add(msg.params[1]) };
});
case irc.RPL_MONONLINE: case irc.RPL_MONONLINE:
case irc.RPL_MONOFFLINE: case irc.RPL_MONOFFLINE:
targets = msg.params[1].split(","); targets = msg.params[1].split(",");

View File

@@ -359,9 +359,6 @@ form input[type="search"] {
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
} }
form label input[type="checkbox"], form label input[type="radio"] {
margin-right: 0.5em;
}
a { a {
color: var(--green); color: var(--green);
@@ -399,7 +396,7 @@ details summary[role="button"] {
white-space: pre-wrap; white-space: pre-wrap;
overflow: auto; overflow: auto;
} }
#buffer .talk, #buffer .motd, #buffer .nag { #buffer .talk, #buffer .motd {
color: var(--main-color); color: var(--main-color);
} }
#buffer .error { #buffer .error {