Compare commits

...

32 Commits

Author SHA1 Message Date
d7c990d5d8 add meta tags 2025-03-01 22:15:23 +01:00
137a7d9014 Change manifest.json 2025-03-01 22:06:37 +01:00
38ac1fa83f Change Name of website 2025-03-01 22:04:52 +01:00
Simon Ser
fcc80a85e3 Only send PING to registered connections on focus 2025-02-28 17:43:13 +01:00
Simon Ser
76b6931ebb components/buffer: flatten separators when appending to children
Nested "key" attributes in arrays don't work. "key" attributes
need to all be in the same array. (Alternatively, we could've used
a fragment and set a key on that.)
2025-02-24 00:29:11 +01:00
Simon Ser
7d068fd1fe Enable @stylistic/js/comma-spacing lint 2025-02-20 17:58:24 +01:00
Simon Ser
735dd8fd8c Enable @stylistic/js/block-spacing lint 2025-02-20 17:57:08 +01:00
Simon Ser
c461f4903e Enable @stylistic/js/arrow-spacing lint 2025-02-20 17:56:19 +01:00
Simon Ser
f897e7d11b Enable @stylistic/js/array-bracket-spacing lint 2025-02-20 17:51:46 +01:00
Simon Ser
95749ba516 Enable @stylistic/js/array-bracket-newline lint 2025-02-20 17:50:35 +01:00
Simon Ser
39a2bc4a3d Enable @stylistic/js/object-curly-newline lint 2025-02-20 17:47:52 +01:00
Simon Ser
614ed5c895 Enable @stylistic/js/brace-style lint 2025-02-20 17:43:17 +01:00
Simon Ser
8d96f93fb5 Enable @stylistic/js/object-curly-spacing lint 2025-02-20 17:42:19 +01:00
Simon Ser
9922d11654 Upgrade @stylistic/eslint-plugin-js to v3 2025-02-17 00:18:13 +01:00
Simon Ser
57c5f2b1cc Upgrade dependencies 2025-02-17 00:15:12 +01:00
Simon Ser
0cc1c53fa4 ci: switch to alpine/latest 2025-02-10 14:09:51 +01:00
Simon Ser
93d7d22726 ci: upload build as artifact 2025-02-10 14:08:43 +01:00
Calvin Lee
136353b2b5 Sort servers alphanumerically 2025-02-07 22:43:03 +00:00
delthas
7dd21177bc Add support for incoming REDACT
This does not include support for redacting messages, only reading
incoming REDACT messages.

See: https://github.com/ircv3/ircv3-specifications/pull/524
2025-02-07 00:26:02 +00:00
Simon Ser
ca0cfdcc28 readme: fix screenshot 2025-02-05 22:40:54 +01:00
vyneer
1e3903c014
Fix /help not showing any commands 2025-01-28 17:03:17 +03:00
Calvin Lee
5146b0cad8 eslint: add lint enforcing camelCase
snake_case is needed in one place in the codebase to format URL arguments.
Co-authored-by: Calvin Lee <pounce@integraldoma.in>
Co-committed-by: Calvin Lee <pounce@integraldoma.in>
2025-01-27 16:29:58 +00:00
Markus Unterwaditzer
513cf825a5 Add nick-caret class
I'd like to apply a userstyle to this text, and in order to do that I need a CSS class.
Co-authored-by: Markus Unterwaditzer <markus-tarpit+git@unterwaditzer.net>
Co-committed-by: Markus Unterwaditzer <markus-tarpit+git@unterwaditzer.net>
2025-01-27 12:25:26 +00:00
Simon Ser
9fef11564d Upgrade dependencies 2025-01-20 23:16:11 +01:00
Simon Ser
9dda4ee438 eslint: add a few more rules 2025-01-20 23:02:23 +01:00
Simon Ser
9299f79bab Make debug=0 URL param disable debug logs 2025-01-19 21:11:58 +01:00
Simon Ser
e4088304bf Upgrade preact, once again
Seems to not cause regressions like it did last time. Crossing
fingers now.
2025-01-07 17:06:08 +01:00
Simon Ser
6ea3601718 dev-server: print remote server errors 2025-01-05 21:52:15 +01:00
emersion
bcf3741ab4 readme: fix screenshot link 2024-12-20 21:37:04 +00:00
Simon Ser
ec5e67336f components/buffer: handle TOPIC clear messages 2024-12-03 09:19:53 +01:00
Umar Getagazov
4d988c98d0 Fix tag name typo in the Timestamp component 2024-11-28 12:37:51 +00:00
Simon Ser
62895d59ff Upgrade dependencies 2024-11-23 21:02:08 +01:00
16 changed files with 1080 additions and 1338 deletions

View File

@ -1,5 +1,4 @@
# TODO switch back to alpine/latest once the "npm install" deadlock is fixed image: alpine/latest
image: alpine/edge
packages: packages:
- npm - npm
- rsync - rsync
@ -7,6 +6,8 @@ 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
@ -14,6 +15,7 @@ tasks:
- 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

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

View File

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

View File

@ -323,6 +323,8 @@ 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) {
@ -760,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.comand !== "QUIT" && msg.command !== irc.RPL_MONONLINE && msg.command !== irc.RPL_MONOFFLINE)) { 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)) {
this.createBuffer(serverID, bufName); this.createBuffer(serverID, bufName);
} }
@ -1073,6 +1075,7 @@ 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:
@ -2003,9 +2006,11 @@ 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"] });
} }
} }
}
componentDidMount() { componentDidMount() {
this.baseTitle = document.title; this.baseTitle = document.title;

View File

@ -43,7 +43,7 @@ function _Timestamp({ date, url, showSeconds }) {
if (showSeconds) { if (showSeconds) {
timestamp += ":--"; timestamp += ":--";
} }
return html`<spam class="timestamp">${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; return this.props.message !== nextProps.message || this.props.redacted !== nextProps.redacted;
} }
render() { render() {
@ -143,12 +143,18 @@ 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 = "-";
} }
content = html`${prefix}${createNick(msg.prefix.name)}${suffix} ${linkify(stripANSI(text), onChannelClick)}`; if (this.props.redacted) {
content = html`<i>This message has been deleted.</i>`;
} else {
content = html`${linkify(stripANSI(text), onChannelClick)}`;
lineClass += " talk";
}
content = html`<span class="nick-caret">${prefix}</span>${createNick(msg.prefix.name)}<span class="nick-caret">${suffix}</span> ${content}`;
} }
let allowedPrefixes = server.statusMsg; let allowedPrefixes = server.statusMsg;
@ -274,9 +280,15 @@ 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];
@ -703,6 +715,7 @@ 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}
@ -808,7 +821,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 = [];
} }

View File

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

View File

@ -63,7 +63,8 @@ if (remoteHost) {
ws.close(); ws.close();
}); });
client.on("error", () => { client.on("error", (err) => {
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`: if set to 1, debug mode is enabled - `debug`: enable debug logs if set to `1`, disable debug logs if set to `0`
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

@ -23,16 +23,34 @@ 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",
"no-throw-literal": "error",
"no-implicit-coercion": "warn", "no-implicit-coercion": "warn",
"object-shorthand": "warn", "object-shorthand": "warn",
"curly": "warn",
"camelcase": "warn",
"@stylistic/js/indent": ["warn", "tab"], "@stylistic/js/indent": ["warn", "tab"],
"@stylistic/js/quotes": ["warn", "double"], "@stylistic/js/quotes": ["warn", "double"],
"@stylistic/js/semi": "warn", "@stylistic/js/semi": "warn",
"@stylistic/js/brace-style": ["warn", "1tbs"],
"@stylistic/js/comma-dangle": ["warn", "always-multiline"], "@stylistic/js/comma-dangle": ["warn", "always-multiline"],
"@stylistic/js/comma-spacing": "warn",
"@stylistic/js/arrow-parens": "warn", "@stylistic/js/arrow-parens": "warn",
"@stylistic/js/arrow-spacing": "warn",
"@stylistic/js/block-spacing": "warn",
"@stylistic/js/object-curly-spacing": ["warn", "always"],
"@stylistic/js/object-curly-newline": ["warn", {
multiline: true,
consistent: true,
}],
"@stylistic/js/array-bracket-spacing": ["warn", "never"],
"@stylistic/js/array-bracket-newline": ["warn", "consistent"],
}, },
}, },
]; ];

View File

@ -3,7 +3,19 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; frame-src 'none'; object-src 'none'; connect-src *;"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; frame-src 'none'; object-src 'none'; connect-src *;">
<title>gamja IRC client</title> <title>Cringe Studios Internet IRC Relay Chat Client</title>
<meta name="description" content="Cringe Studios Internet IRC Relay Chat Client">
<meta property="og:image" content="https://nsfw.cringe-studios.com/Wikimedia_Community_Logo-IRC.png">
<meta property="og:type" content="website">
<meta property="og:url" content="https://irc.cringe-studios.com/">
<meta property="og:title" content="Cringe Studios Internet IRC Relay Chat Client">
<meta property="og:description" content="Cringe Studios Internet IRC Relay Chat Client">
<meta name="theme-color" content="#8000f0" data-react-helmet="true">
<link rel="icon" href="https://nsfw.cringe-studios.com/Wikimedia_Community_Logo-IRC.png">
<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"> <meta name="viewport" content="width=device-width, initial-scale=1">

View File

@ -21,6 +21,7 @@ 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",

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

View File

@ -1,6 +1,6 @@
{ {
"name": "gamja IRC client", "name": "Cringe Studios Internet IRC Relay Chat Client",
"short_name": "gamja", "short_name": "IRC",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"scope": "." "scope": "."

2272
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,13 @@
"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-js": "^2.8.0", "@stylistic/eslint-plugin-js": "^3.1.0",
"eslint": "^9.11.1", "eslint": "^9.11.1",
"globals": "^15.9.0", "globals": "^15.9.0",
"node-static": "^0.7.11", "node-static": "^0.7.11",

View File

@ -153,10 +153,24 @@ 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(a, b) { function compareBuffers(state, 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)) {
@ -361,10 +375,11 @@ 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(compareBuffers); bufferList = bufferList.sort((a, b) => compareBuffers(state, a, b));
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 }];
}, },
@ -665,6 +680,14 @@ export const State = {
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(",");