Compare commits
56 Commits
eslint-ext
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 77ccba04ba | |||
| e68534a846 | |||
| 5b96dd0d8b | |||
| 4c245ad49e | |||
|
|
bdfde7f1e0 | ||
|
|
223b9b1531 | ||
|
|
8aae4a2b07 | ||
|
|
910ce284a2 | ||
|
|
1e630aed0d | ||
|
|
b39b46fa12 | ||
|
|
65a0a34fa1 | ||
|
|
642e90f51c | ||
|
|
64c2325db8 | ||
|
|
700919b5c4 | ||
|
|
e91c246a95 | ||
|
|
5b7459f24d | ||
|
|
af3a255824 | ||
|
|
7e785ed101 | ||
|
|
02cc554df6 | ||
|
|
6aef3e906b | ||
|
|
9879dbc722 | ||
|
|
afbd7c0bb3 | ||
|
|
e09541ad2f | ||
|
|
6905b9d768 | ||
|
|
caf6e9978b | ||
|
|
fbfa123dca | ||
|
|
cd45ead256 | ||
|
|
fcc80a85e3 | ||
|
|
76b6931ebb | ||
|
|
7d068fd1fe | ||
|
|
735dd8fd8c | ||
|
|
c461f4903e | ||
|
|
f897e7d11b | ||
|
|
95749ba516 | ||
|
|
39a2bc4a3d | ||
|
|
614ed5c895 | ||
|
|
8d96f93fb5 | ||
|
|
9922d11654 | ||
|
|
57c5f2b1cc | ||
|
|
0cc1c53fa4 | ||
|
|
93d7d22726 | ||
|
|
136353b2b5 | ||
|
|
7dd21177bc | ||
|
|
ca0cfdcc28 | ||
|
|
1e3903c014 | ||
|
|
5146b0cad8 | ||
|
|
513cf825a5 | ||
|
|
9fef11564d | ||
|
|
9dda4ee438 | ||
|
|
9299f79bab | ||
|
|
e4088304bf | ||
|
|
6ea3601718 | ||
|
|
bcf3741ab4 | ||
|
|
ec5e67336f | ||
|
|
4d988c98d0 | ||
|
|
62895d59ff |
@@ -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,13 +6,16 @@ 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 install --include=dev
|
npm clean-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
|
||||||
|
|||||||
39
.gitea/workflows/build.yml
Normal file
39
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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">
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -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,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" },
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ 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);
|
||||||
@@ -323,6 +324,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) {
|
||||||
@@ -665,12 +668,6 @@ 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);
|
||||||
|
|
||||||
@@ -760,7 +757,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 +1070,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:
|
||||||
@@ -1797,7 +1795,12 @@ export default class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let msg of result.messages) {
|
for (let msg of result.messages) {
|
||||||
this.addChatMessage(buf.server, buf.name, msg);
|
this.prepareChatMessage(buf.server, 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2003,7 +2006,9 @@ 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()) {
|
||||||
client.send({ command: "PING", params: ["gamja"] });
|
if (client.status === Client.Status.REGISTERED) {
|
||||||
|
client.send({ command: "PING", params: ["gamja"] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2067,7 +2072,7 @@ export default class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bufferHeader = html`
|
bufferHeader = html`
|
||||||
<section id="buffer-header">
|
<section id="buffer-header" role="banner">
|
||||||
<${BufferHeader}
|
<${BufferHeader}
|
||||||
buffer=${activeBuffer}
|
buffer=${activeBuffer}
|
||||||
server=${activeServer}
|
server=${activeServer}
|
||||||
@@ -2089,8 +2094,10 @@ 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"
|
||||||
@@ -2217,7 +2224,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">
|
<div id="error-msg" role="alert">
|
||||||
${this.state.error}
|
${this.state.error}
|
||||||
${" "}
|
${" "}
|
||||||
<button onClick=${this.handleDismissError}>×</button>
|
<button onClick=${this.handleDismissError}>×</button>
|
||||||
@@ -2241,8 +2248,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}
|
||||||
@@ -2267,7 +2274,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">
|
<section id="buffer" ref=${this.buffer} tabindex="-1" role="log">
|
||||||
<${Buffer}
|
<${Buffer}
|
||||||
buffer=${activeBuffer}
|
buffer=${activeBuffer}
|
||||||
server=${activeServer}
|
server=${activeServer}
|
||||||
@@ -2286,6 +2293,7 @@ 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}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function BufferItem(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<li class="${classes.join(" ")}">
|
<li class="${classes.join(" ")}" role="tab" aria-selected="${props.active}">
|
||||||
<a
|
<a
|
||||||
href=${getBufferURL(props.buffer)}
|
href=${getBufferURL(props.buffer)}
|
||||||
title=${title}
|
title=${title}
|
||||||
@@ -80,5 +80,9 @@ export default function BufferList(props) {
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return html`<ul>${items}</ul>`;
|
return html`
|
||||||
|
<ul role="tablist" aria-label="Buffer list">
|
||||||
|
${items}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" aria-hidden="true">${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,24 @@ 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" 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;
|
||||||
@@ -274,9 +286,15 @@ class LogLine extends Component {
|
|||||||
break;
|
break;
|
||||||
case "TOPIC":
|
case "TOPIC":
|
||||||
let topic = msg.params[1];
|
let topic = msg.params[1];
|
||||||
content = html`
|
if (topic) {
|
||||||
${createNick(msg.prefix.name)} changed the topic to: ${linkify(stripANSI(topic), onChannelClick)}
|
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;
|
break;
|
||||||
case "INVITE":
|
case "INVITE":
|
||||||
invitee = msg.params[0];
|
invitee = msg.params[0];
|
||||||
@@ -368,7 +386,7 @@ class LogLine extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline ${lineClass}" data-key=${msg.key}>
|
<div class="logline ${lineClass}" data-key=${msg.key} role="listitem">
|
||||||
<${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/>
|
<${Timestamp} date=${new Date(msg.tags.time)} url=${getMessageURL(buf, msg)}/>
|
||||||
${" "}
|
${" "}
|
||||||
${content}
|
${content}
|
||||||
@@ -493,7 +511,7 @@ class FoldGroup extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline" data-key=${msgs[0].key}>
|
<div class="logline" data-key=${msgs[0].key} role="listitem">
|
||||||
${timestamp}
|
${timestamp}
|
||||||
${" "}
|
${" "}
|
||||||
${content}
|
${content}
|
||||||
@@ -546,7 +564,7 @@ class NotificationNagger extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline">
|
<div class="logline nag" role="listitem">
|
||||||
<${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
|
||||||
@@ -587,7 +605,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">
|
<div class="logline nag" role="listitem">
|
||||||
<${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}
|
||||||
@@ -625,7 +643,7 @@ function AccountNagger({ server, onAuthClick, onRegisterClick }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline">
|
<div class="logline nag" role="listitem">
|
||||||
<${Timestamp}/> ${msg}
|
<${Timestamp}/> ${msg}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -644,7 +662,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">
|
<div class="separator date-separator" role="separator">
|
||||||
${text}
|
${text}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -652,7 +670,7 @@ class DateSeparator extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UnreadSeparator(props) {
|
function UnreadSeparator(props) {
|
||||||
return html`<div class="separator unread-separator">New messages</div>`;
|
return html`<div class="separator unread-separator" role="separator">New messages</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameDate(d1, d2) {
|
function sameDate(d1, d2) {
|
||||||
@@ -703,6 +721,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 +827,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 = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,7 +847,7 @@ export default class Buffer extends Component {
|
|||||||
children.push(createFoldGroup(foldMessages));
|
children.push(createFoldGroup(foldMessages));
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logline-list">
|
<div class="logline-list" role="list">
|
||||||
${children}
|
${children}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -185,7 +185,13 @@ export default class Composer extends Component {
|
|||||||
promises.push(this.uploadFile(file));
|
promises.push(this.uploadFile(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
let urls = await Promise.all(promises);
|
let urls;
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ export default class Dialog extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="dialog" onClick=${this.handleBackdropClick}>
|
<div class="dialog" onClick=${this.handleBackdropClick} role="dialog" aria-modal="true">
|
||||||
<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}>×</button>
|
<button class="dialog-close" onClick=${this.handleCloseClick} title="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
${this.props.children}
|
${this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.handleKeyUp = this.handleKeyUp.bind(this);
|
this.handleKeyDown = this.handleKeyDown.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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyUp(event) {
|
handleKeyDown(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}
|
||||||
onKeyUp=${this.handleKeyUp}
|
onKeyDown=${this.handleKeyDown}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import stylisticJs from "@stylistic/eslint-plugin-js";
|
import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@@ -14,7 +14,7 @@ export default [
|
|||||||
"process": "readonly",
|
"process": "readonly",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: { "@stylistic/js": stylisticJs },
|
plugins: { "@stylistic": stylistic },
|
||||||
rules: {
|
rules: {
|
||||||
"no-case-declarations": "off",
|
"no-case-declarations": "off",
|
||||||
"no-unused-vars": ["error", {
|
"no-unused-vars": ["error", {
|
||||||
@@ -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",
|
||||||
"@stylistic/js/indent": ["warn", "tab"],
|
"curly": "warn",
|
||||||
"@stylistic/js/quotes": ["warn", "double"],
|
"camelcase": "warn",
|
||||||
"@stylistic/js/semi": "warn",
|
"@stylistic/indent": ["warn", "tab", { SwitchCase: 0 }],
|
||||||
"@stylistic/js/comma-dangle": ["warn", "always-multiline"],
|
"@stylistic/quotes": ["warn", "double"],
|
||||||
"@stylistic/js/arrow-parens": "warn",
|
"@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/array-bracket-newline": ["warn", "consistent"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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">
|
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -31,9 +32,33 @@ 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 NORMAL_CLOSURE = 1000;
|
const WEBSOCKET_CLOSE_CODES = {
|
||||||
const GOING_AWAY = 1001;
|
NORMAL_CLOSURE: 1000,
|
||||||
const UNSUPPORTED_DATA = 1003;
|
GOING_AWAY: 1001,
|
||||||
|
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
|
||||||
@@ -68,6 +93,18 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -188,8 +225,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 !== NORMAL_CLOSURE && event.code !== GOING_AWAY) {
|
if (event.code !== WEBSOCKET_CLOSE_CODES.NORMAL_CLOSURE && event.code !== WEBSOCKET_CLOSE_CODES.GOING_AWAY) {
|
||||||
this.dispatchError(new Error("Connection error"));
|
this.dispatchError(new WebSocketError(event.code));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
@@ -236,7 +273,7 @@ export default class Client extends EventTarget {
|
|||||||
this.setPingInterval(0);
|
this.setPingInterval(0);
|
||||||
|
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close(NORMAL_CLOSURE);
|
this.ws.close(WEBSOCKET_CLOSE_CODES.NORMAL_CLOSURE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +333,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(UNSUPPORTED_DATA);
|
this.ws.close(WEBSOCKET_CLOSE_CODES.UNSUPPORTED_DATA);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.es.js";
|
import * as linkifyjs from "../node_modules/linkifyjs/dist/linkify.mjs";
|
||||||
export { linkifyjs };
|
export { linkifyjs };
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
3035
package-lock.json
generated
3035
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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-js": "^2.8.0",
|
"@stylistic/eslint-plugin": "^5.1.0",
|
||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"globals": "^15.9.0",
|
"globals": "^17.0.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",
|
||||||
|
|||||||
48
state.js
48
state.js
@@ -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)) {
|
||||||
@@ -217,7 +231,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;
|
||||||
}
|
}
|
||||||
@@ -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 }];
|
||||||
},
|
},
|
||||||
@@ -596,11 +611,12 @@ export const State = {
|
|||||||
if (buf.server !== serverID) {
|
if (buf.server !== serverID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!buf.members.has(msg.prefix.name)) {
|
let membership = members.get(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, members.get(msg.prefix.name));
|
members.set(newNick, membership);
|
||||||
members.delete(msg.prefix.name);
|
members.delete(msg.prefix.name);
|
||||||
buffers.set(buf.id, { ...buf, members });
|
buffers.set(buf.id, { ...buf, members });
|
||||||
});
|
});
|
||||||
@@ -655,16 +671,28 @@ 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)) {
|
||||||
let nick = arg;
|
return;
|
||||||
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(",");
|
||||||
|
|||||||
@@ -359,6 +359,9 @@ 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);
|
||||||
@@ -396,7 +399,7 @@ details summary[role="button"] {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
#buffer .talk, #buffer .motd {
|
#buffer .talk, #buffer .motd, #buffer .nag {
|
||||||
color: var(--main-color);
|
color: var(--main-color);
|
||||||
}
|
}
|
||||||
#buffer .error {
|
#buffer .error {
|
||||||
|
|||||||
Reference in New Issue
Block a user