1 Commits

Author SHA1 Message Date
Simon Ser
e3319919a1 wip 2025-01-20 23:05:43 +01:00
20 changed files with 1384 additions and 1359 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,39 +0,0 @@
name: Bun Package
on:
push:
branches: [ master ]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout código
run: |
git clone https://fedesrv.ddns.net/git/${{ github.repository }}.git .
git checkout ${{ github.sha }}
- 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"> <img src="https://codeberg.org/attachments/117ff232-7e73-46c7-a21d-2b59ffa3765a" alt="Screenshot" width="800">
## 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);
@@ -668,6 +667,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 +762,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 +1075,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 +1799,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 +2005,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 +2069,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 +2091,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 +2219,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 +2243,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 +2269,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 +2288,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`<span 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;
@@ -386,7 +374,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 +499,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 +552,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 +593,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 +631,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 +650,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 +658,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 +709,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 +814,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 +834,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

@@ -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", {
@@ -34,23 +34,15 @@ export default [
"no-implicit-coercion": "warn", "no-implicit-coercion": "warn",
"object-shorthand": "warn", "object-shorthand": "warn",
"curly": "warn", "curly": "warn",
"camelcase": "warn", "no-restricted-syntax": ["warn", {
"@stylistic/indent": ["warn", "tab", { SwitchCase: 0 }], selector: "BlockStatement VariableDeclaration[kind='const']",
"@stylistic/quotes": ["warn", "double"], message: "NOPE",
"@stylistic/semi": "warn",
"@stylistic/brace-style": ["warn", "1tbs"],
"@stylistic/comma-dangle": ["warn", "always-multiline"],
"@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/js/indent": ["warn", "tab"],
"@stylistic/array-bracket-newline": ["warn", "consistent"], "@stylistic/js/quotes": ["warn", "double"],
"@stylistic/js/semi": "warn",
"@stylistic/js/comma-dangle": ["warn", "always-multiline"],
"@stylistic/js/arrow-parens": "warn",
}, },
}, },
]; ];

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, {

2426
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,9 @@
"@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 {