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/edge
image: alpine/latest
packages:
- npm
- rsync
@ -7,6 +6,8 @@ sources:
- https://codeberg.org/emersion/gamja.git
secrets:
- 7a146c8e-aeb4-46e7-99bf-05af7486bbe9 # deploy SSH key
artifacts:
- gamja/gamja.tar.gz
tasks:
- setup: |
cd gamja
@ -14,6 +15,7 @@ tasks:
- build: |
cd gamja
npm run build
tar -czf gamja.tar.gz -C dist .
- lint: |
cd gamja
npm run -- lint --max-warnings 0

View File

@ -2,7 +2,7 @@
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

View File

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

View File

@ -323,6 +323,8 @@ export default class App extends Component {
}
if (queryParams.debug === "1") {
this.debug = true;
} else if (queryParams.debug === "0") {
this.debug = false;
}
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
// 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);
}
@ -1073,6 +1075,7 @@ export default class App extends Component {
case "ACK":
case "BOUNCER":
case "MARKREAD":
case "REDACT":
// Ignore these
return [];
default:
@ -2003,7 +2006,9 @@ export default class App extends Component {
this.lastFocusPingDate = now;
for (let client of this.clients.values()) {
client.send({ command: "PING", params: ["gamja"] });
if (client.status === Client.Status.REGISTERED) {
client.send({ command: "PING", params: ["gamja"] });
}
}
}

View File

@ -43,7 +43,7 @@ function _Timestamp({ date, url, showSeconds }) {
if (showSeconds) {
timestamp += ":--";
}
return html`<spam class="timestamp">${timestamp}</span>`;
return html`<span class="timestamp">${timestamp}</span>`;
}
let hh = date.getHours().toString().padStart(2, "0");
@ -94,7 +94,7 @@ function canFoldMessage(msg) {
class LogLine extends Component {
shouldComponentUpdate(nextProps) {
return this.props.message !== nextProps.message;
return this.props.message !== nextProps.message || this.props.redacted !== nextProps.redacted;
}
render() {
@ -143,12 +143,18 @@ class LogLine extends Component {
`;
}
} else {
lineClass = "talk";
let prefix = "<", suffix = ">";
if (msg.command === "NOTICE") {
lineClass += " notice";
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;
@ -274,9 +280,15 @@ class LogLine extends Component {
break;
case "TOPIC":
let topic = msg.params[1];
content = html`
${createNick(msg.prefix.name)} changed the topic to: ${linkify(stripANSI(topic), onChannelClick)}
`;
if (topic) {
content = html`
${createNick(msg.prefix.name)} changed the topic to: ${linkify(stripANSI(topic), onChannelClick)}
`;
} else {
content = html`
${createNick(msg.prefix.name)} cleared the topic
`;
}
break;
case "INVITE":
invitee = msg.params[0];
@ -703,6 +715,7 @@ export default class Buffer extends Component {
message=${msg}
buffer=${buf}
server=${server}
redacted=${buf.redacted.has(msg.tags.msgid)}
onChannelClick=${onChannelClick}
onNickClick=${onNickClick}
onVerifyClick=${onVerifyClick}
@ -808,7 +821,7 @@ export default class Buffer extends Component {
if (sep.length > 0) {
children.push(createFoldGroup(foldMessages));
children.push(sep);
children.push(...sep);
foldMessages = [];
}

View File

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

View File

@ -63,7 +63,8 @@ if (remoteHost) {
ws.close();
});
client.on("error", () => {
client.on("error", (err) => {
console.log(err);
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)
- `channels`: comma-separated list of channels to join (`#` needs to be escaped)
- `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
appending the channel name to the gamja URL).

View File

@ -23,16 +23,34 @@ export default [
destructuredArrayIgnorePattern: "^_",
}],
"no-var": "error",
"no-eval": "error",
"no-implied-eval": "error",
"eqeqeq": "error",
"no-invalid-this": "error",
"no-extend-native": "error",
"prefer-arrow-callback": "error",
"no-implicit-globals": "error",
"no-throw-literal": "error",
"no-implicit-coercion": "warn",
"object-shorthand": "warn",
"curly": "warn",
"camelcase": "warn",
"@stylistic/js/indent": ["warn", "tab"],
"@stylistic/js/quotes": ["warn", "double"],
"@stylistic/js/semi": "warn",
"@stylistic/js/brace-style": ["warn", "1tbs"],
"@stylistic/js/comma-dangle": ["warn", "always-multiline"],
"@stylistic/js/comma-spacing": "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>
<meta charset="utf-8">
<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">
<script type="module" src="./main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">

View File

@ -21,6 +21,7 @@ const permanentCaps = [
"draft/account-registration",
"draft/chathistory",
"draft/extended-monitor",
"draft/message-redaction",
"draft/read-marker",
"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
// forgery
let params = {
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
"response_type": "code",
"client_id": clientId,
"redirect_uri": redirectUri,
};
if (scope) {
params.scope = scope;
@ -66,12 +66,12 @@ function buildPostHeaders(clientId, clientSecret) {
export async function exchangeCode({ serverMetadata, redirectUri, code, clientId, clientSecret }) {
let data = {
grant_type: "authorization_code",
"grant_type": "authorization_code",
code,
redirect_uri: redirectUri,
"redirect_uri": redirectUri,
};
if (!clientSecret) {
data.client_id = clientId;
data["client_id"] = clientId;
}
let resp = await fetch(serverMetadata.token_endpoint, {

View File

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

2272
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,13 @@
"dependencies": {
"htm": "^3.0.4",
"linkifyjs": "^4.1.3",
"preact": "10.17.1"
"preact": "^10.17.1"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@parcel/packager-raw-url": "^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",
"globals": "^15.9.0",
"node-static": "^0.7.11",

View File

@ -153,10 +153,24 @@ function trimStartCharacter(s, c) {
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
* 0 otherwise. */
function compareBuffers(a, b) {
function compareBuffers(state, a, b) {
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;
}
if (isServerBuffer(a) !== isServerBuffer(b)) {
@ -217,7 +231,7 @@ function insertMessage(list, msg) {
}
console.assert(insertBefore >= 0, "");
list = [ ...list ];
list = [...list];
list.splice(insertBefore, 0, msg);
return list;
}
@ -361,10 +375,11 @@ export const State = {
hasInitialWho: false, // if channel
members: new irc.CaseMapMap(null, client.cm), // if channel
messages: [],
redacted: new Set(),
unread: Unread.NONE,
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]));
return [id, { buffers }];
},
@ -665,6 +680,14 @@ export const State = {
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_MONOFFLINE:
targets = msg.params[1].split(",");