Compare commits
16 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54453c5f44 | ||
|
|
fa80a56516 | ||
|
|
7cabb6f85b | ||
|
|
505a6fd5ab | ||
|
|
8e30806fec | ||
|
|
f0c398a10c | ||
|
|
baaf576d82 | ||
|
|
e3c2d85a94 | ||
|
|
576b9d51eb | ||
|
|
6b04cb1417 | ||
|
|
8507500d74 | ||
|
|
aaef4e1629 | ||
|
|
cdd2da90a9 | ||
|
|
4a981997f0 | ||
|
|
f45b51d981 | ||
|
|
73db1a888e |
32
commands.js
32
commands.js
@@ -83,6 +83,22 @@ const kick = {
|
||||
},
|
||||
};
|
||||
|
||||
const ban = {
|
||||
usage: "[nick]",
|
||||
description: "Ban a user from the channel, or display the current ban list",
|
||||
execute: (app, args) => {
|
||||
if (args.length == 0) {
|
||||
let activeChannel = getActiveChannel(app);
|
||||
getActiveClient(app).send({
|
||||
command: "MODE",
|
||||
params: [activeChannel, "+b"],
|
||||
});
|
||||
} else {
|
||||
return setUserHostMode(app, args, "+b");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function givemode(app, args, mode) {
|
||||
// TODO: Handle several users at once
|
||||
let nick = args[0];
|
||||
@@ -108,21 +124,7 @@ export default {
|
||||
getActiveClient(app).send({command: "AWAY", params});
|
||||
},
|
||||
},
|
||||
"ban": {
|
||||
usage: "[nick]",
|
||||
description: "Ban a user from the channel, or display the current ban list",
|
||||
execute: (app, args) => {
|
||||
if (args.length == 0) {
|
||||
let activeChannel = getActiveChannel(app);
|
||||
getActiveClient(app).send({
|
||||
command: "MODE",
|
||||
params: [activeChannel, "+b"],
|
||||
});
|
||||
} else {
|
||||
return setUserHostMode(app, args, "+b");
|
||||
}
|
||||
},
|
||||
},
|
||||
"ban": ban,
|
||||
"buffer": {
|
||||
usage: "<name>",
|
||||
description: "Switch to a buffer",
|
||||
|
||||
@@ -11,12 +11,13 @@ import NetworkForm from "./network-form.js";
|
||||
import AuthForm from "./auth-form.js";
|
||||
import RegisterForm from "./register-form.js";
|
||||
import VerifyForm from "./verify-form.js";
|
||||
import SettingsForm from "./settings-form.js";
|
||||
import Composer from "./composer.js";
|
||||
import ScrollManager from "./scroll-manager.js";
|
||||
import Dialog from "./dialog.js";
|
||||
import { html, Component, createRef } from "../lib/index.js";
|
||||
import { strip as stripANSI } from "../lib/ansi.js";
|
||||
import { SERVER_BUFFER, BufferType, ReceiptType, ServerStatus, Unread, State, getServerName, receiptFromMessage, isReceiptBefore, isMessageBeforeReceipt } from "../state.js";
|
||||
import { SERVER_BUFFER, BufferType, ReceiptType, ServerStatus, Unread, BufferEventsDisplayMode, State, getServerName, receiptFromMessage, isReceiptBefore, isMessageBeforeReceipt, SettingsContext } from "../state.js";
|
||||
import commands from "../commands.js";
|
||||
import { setup as setupKeybindings } from "../keybindings.js";
|
||||
import * as store from "../store.js";
|
||||
@@ -196,6 +197,7 @@ export default class App extends Component {
|
||||
*/
|
||||
autoOpenURL = null;
|
||||
messageNotifications = new Set();
|
||||
baseTitle = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -220,6 +222,13 @@ export default class App extends Component {
|
||||
this.handleRegisterSubmit = this.handleRegisterSubmit.bind(this);
|
||||
this.handleVerifyClick = this.handleVerifyClick.bind(this);
|
||||
this.handleVerifySubmit = this.handleVerifySubmit.bind(this);
|
||||
this.handleOpenSettingsClick = this.handleOpenSettingsClick.bind(this);
|
||||
this.handleSettingsChange = this.handleSettingsChange.bind(this);
|
||||
|
||||
this.state.settings = {
|
||||
...this.state.settings,
|
||||
...store.settings.load(),
|
||||
};
|
||||
|
||||
this.bufferStore = new store.Buffer();
|
||||
|
||||
@@ -470,6 +479,12 @@ export default class App extends Component {
|
||||
if (buf.type === BufferType.NICK && !server.users.has(buf.name)) {
|
||||
this.whoUserBuffer(buf.name, buf.server);
|
||||
}
|
||||
|
||||
if (buf.type !== BufferType.SERVER) {
|
||||
document.title = buf.name + ' · ' + this.baseTitle;
|
||||
} else {
|
||||
document.title = this.baseTitle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -610,6 +625,13 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
connect(params) {
|
||||
// Merge our previous connection params so that config options such as
|
||||
// the ping interval are applied
|
||||
params = {
|
||||
...this.state.connectParams,
|
||||
...params,
|
||||
};
|
||||
|
||||
let serverID = null;
|
||||
this.setState((state) => {
|
||||
let update;
|
||||
@@ -618,7 +640,10 @@ export default class App extends Component {
|
||||
});
|
||||
this.setState({ connectParams: params });
|
||||
|
||||
let client = new Client(fillConnectParams(params));
|
||||
let client = new Client({
|
||||
...fillConnectParams(params),
|
||||
eventPlayback: this.state.settings.bufferEvents !== BufferEventsDisplayMode.HIDE,
|
||||
});
|
||||
client.debug = this.debug;
|
||||
|
||||
this.clients.set(serverID, client);
|
||||
@@ -851,15 +876,12 @@ export default class App extends Component {
|
||||
handleMessage(serverID, msg) {
|
||||
let client = this.clients.get(serverID);
|
||||
|
||||
let destBuffers = this.routeMessage(serverID, msg);
|
||||
|
||||
if (irc.findBatchByType(msg, "chathistory")) {
|
||||
destBuffers.forEach((bufName) => {
|
||||
this.addMessage(serverID, bufName, msg);
|
||||
});
|
||||
return;
|
||||
return; // Handled by the caller
|
||||
}
|
||||
|
||||
let destBuffers = this.routeMessage(serverID, msg);
|
||||
|
||||
this.setState((state) => State.handleMessage(state, msg, serverID, client));
|
||||
|
||||
let target, channel;
|
||||
@@ -1090,7 +1112,14 @@ export default class App extends Component {
|
||||
from = receiptFromMessage(lastMsg);
|
||||
}
|
||||
|
||||
client.fetchHistoryBetween(target.name, from, to, CHATHISTORY_MAX_SIZE).catch((err) => {
|
||||
client.fetchHistoryBetween(target.name, from, to, CHATHISTORY_MAX_SIZE).then((result) => {
|
||||
for (let msg of result.messages) {
|
||||
let destBuffers = this.routeMessage(serverID, msg);
|
||||
for (let bufName of destBuffers) {
|
||||
this.addMessage(serverID, target.name, msg);
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error("Failed to fetch backlog for '" + target.name + "': ", err);
|
||||
this.showError("Failed to fetch backlog for '" + target.name + "'");
|
||||
});
|
||||
@@ -1287,7 +1316,9 @@ export default class App extends Component {
|
||||
}
|
||||
// fallthrough
|
||||
case BufferType.NICK:
|
||||
this.switchBuffer({ name: SERVER_BUFFER });
|
||||
if (this.state.activeBuffer === buf.id) {
|
||||
this.switchBuffer({ name: SERVER_BUFFER });
|
||||
}
|
||||
this.setState((state) => {
|
||||
let buffers = new Map(state.buffers);
|
||||
buffers.delete(buf.id);
|
||||
@@ -1301,6 +1332,10 @@ export default class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
disconnectAll() {
|
||||
this.close(this.state.buffers.keys().next().value);
|
||||
}
|
||||
|
||||
executeCommand(s) {
|
||||
let parts = s.split(" ");
|
||||
let name = parts[0].toLowerCase().slice(1);
|
||||
@@ -1496,6 +1531,9 @@ export default class App extends Component {
|
||||
|
||||
client.fetchHistoryBefore(buf.name, before, limit).then((result) => {
|
||||
this.endOfHistory.set(buf.id, !result.more);
|
||||
for (let msg of result.messages) {
|
||||
this.addMessage(buf.server, buf.name, msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1658,10 +1696,32 @@ export default class App extends Component {
|
||||
this.dismissDialog();
|
||||
}
|
||||
|
||||
handleOpenSettingsClick() {
|
||||
let showProtocolHandler = false;
|
||||
for (let [id, client] of this.clients) {
|
||||
if (client.caps.enabled.has("soju.im/bouncer-networks")) {
|
||||
showProtocolHandler = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.openDialog("settings", { showProtocolHandler });
|
||||
}
|
||||
|
||||
handleSettingsChange(settings) {
|
||||
store.settings.put(settings);
|
||||
this.setState({ settings });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.baseTitle = document.title;
|
||||
setupKeybindings(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.title = this.baseTitle;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return html`<section id="connect"></section>`;
|
||||
@@ -1714,6 +1774,7 @@ export default class App extends Component {
|
||||
onReconnect=${() => this.reconnect()}
|
||||
onAddNetwork=${this.handleAddNetworkClick}
|
||||
onManageNetwork=${() => this.handleManageNetworkClick(activeBuffer.server)}
|
||||
onOpenSettings=${this.handleOpenSettingsClick}
|
||||
/>
|
||||
</section>
|
||||
`;
|
||||
@@ -1822,6 +1883,19 @@ export default class App extends Component {
|
||||
</>
|
||||
`;
|
||||
break;
|
||||
case "settings":
|
||||
dialog = html`
|
||||
<${Dialog} title="Settings" onDismiss=${this.dismissDialog}>
|
||||
<${SettingsForm}
|
||||
settings=${this.state.settings}
|
||||
showProtocolHandler=${dialogData.showProtocolHandler}
|
||||
onChange=${this.handleSettingsChange}
|
||||
onDisconnect=${() => this.disconnectAll()}
|
||||
onClose=${() => this.dismissDialog()}
|
||||
/>
|
||||
</>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
let error = null;
|
||||
@@ -1845,7 +1919,7 @@ export default class App extends Component {
|
||||
commandOnly = true;
|
||||
}
|
||||
|
||||
return html`
|
||||
let app = html`
|
||||
<section
|
||||
id="buffer-list"
|
||||
class=${this.state.openPanels.bufferList ? "expand" : ""}
|
||||
@@ -1878,6 +1952,7 @@ export default class App extends Component {
|
||||
buffer=${activeBuffer}
|
||||
server=${activeServer}
|
||||
bouncerNetwork=${activeBouncerNetwork}
|
||||
settings=${this.state.settings}
|
||||
onChannelClick=${this.handleChannelClick}
|
||||
onNickClick=${this.handleNickClick}
|
||||
onAuthClick=${() => this.handleAuthClick(activeBuffer.server)}
|
||||
@@ -1897,5 +1972,11 @@ export default class App extends Component {
|
||||
${dialog}
|
||||
${error}
|
||||
`;
|
||||
|
||||
return html`
|
||||
<${SettingsContext.Provider} value=${this.state.settings}>
|
||||
${app}
|
||||
</>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ export default function BufferHeader(props) {
|
||||
switch (props.bouncerNetwork.state) {
|
||||
case "disconnected":
|
||||
description = "Bouncer disconnected from network";
|
||||
if (props.bouncerNetwork.error) {
|
||||
description += ": " + props.bouncerNetwork.error;
|
||||
}
|
||||
break;
|
||||
case "connecting":
|
||||
description = "Bouncer connecting to network...";
|
||||
@@ -74,6 +77,12 @@ export default function BufferHeader(props) {
|
||||
onClick=${props.onReconnect}
|
||||
>Reconnect</button>
|
||||
`;
|
||||
let settingsButton = html`
|
||||
<button
|
||||
key="settings"
|
||||
onClick="${props.onOpenSettings}"
|
||||
>Settings</button>
|
||||
`;
|
||||
|
||||
if (props.server.isBouncer) {
|
||||
if (props.server.bouncerNetID) {
|
||||
@@ -99,27 +108,16 @@ export default function BufferHeader(props) {
|
||||
} else if (props.server.status === ServerStatus.DISCONNECTED) {
|
||||
actions.push(reconnectButton);
|
||||
}
|
||||
actions.push(html`
|
||||
<button
|
||||
key="disconnect"
|
||||
class="danger"
|
||||
onClick=${props.onClose}
|
||||
>Disconnect</button>
|
||||
`);
|
||||
actions.push(settingsButton);
|
||||
}
|
||||
} else {
|
||||
if (fullyConnected) {
|
||||
actions.push(joinButton);
|
||||
actions.push(settingsButton);
|
||||
} else if (props.server.status === ServerStatus.DISCONNECTED) {
|
||||
actions.push(reconnectButton);
|
||||
}
|
||||
actions.push(html`
|
||||
<button
|
||||
key="disconnect"
|
||||
class="danger"
|
||||
onClick=${props.onClose}
|
||||
>Disconnect</button>
|
||||
`);
|
||||
actions.push(settingsButton);
|
||||
}
|
||||
break;
|
||||
case BufferType.CHANNEL:
|
||||
@@ -189,6 +187,10 @@ export default function BufferHeader(props) {
|
||||
let desc = "This user is a server operator, they have administrator privileges.";
|
||||
details.push(html`<abbr title=${desc}>server operator</abbr>`);
|
||||
}
|
||||
if (props.user.bot) {
|
||||
let desc = "This user is an automated bot.";
|
||||
details.push(html`<abbr title=${desc}>bot</abbr>`);
|
||||
}
|
||||
details = details.map((item, i) => {
|
||||
if (i === 0) {
|
||||
return item;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { html, Component } from "../lib/index.js";
|
||||
import linkify from "../lib/linkify.js";
|
||||
import * as irc from "../lib/irc.js";
|
||||
import { strip as stripANSI } from "../lib/ansi.js";
|
||||
import { BufferType, ServerStatus, getNickURL, getChannelURL, getMessageURL, isMessageBeforeReceipt } from "../state.js";
|
||||
import { BufferType, ServerStatus, BufferEventsDisplayMode, getNickURL, getChannelURL, getMessageURL, isMessageBeforeReceipt, SettingsContext } from "../state.js";
|
||||
import * as store from "../store.js";
|
||||
import Membership from "./membership.js";
|
||||
|
||||
@@ -27,15 +27,22 @@ function Nick(props) {
|
||||
`;
|
||||
}
|
||||
|
||||
function Timestamp({ date, url }) {
|
||||
function _Timestamp({ date, url, showSeconds }) {
|
||||
if (!date) {
|
||||
return html`<spam class="timestamp">--:--:--</span>`;
|
||||
let timestamp = "--:--";
|
||||
if (showSeconds) {
|
||||
timestamp += ":--";
|
||||
}
|
||||
return html`<spam class="timestamp">${timestamp}</span>`;
|
||||
}
|
||||
|
||||
let hh = date.getHours().toString().padStart(2, "0");
|
||||
let mm = date.getMinutes().toString().padStart(2, "0");
|
||||
let ss = date.getSeconds().toString().padStart(2, "0");
|
||||
let timestamp = `${hh}:${mm}:${ss}`;
|
||||
let timestamp = `${hh}:${mm}`;
|
||||
if (showSeconds) {
|
||||
let ss = date.getSeconds().toString().padStart(2, "0");
|
||||
timestamp += ":" + ss;
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
href=${url}
|
||||
@@ -48,6 +55,16 @@ function Timestamp({ date, url }) {
|
||||
`;
|
||||
}
|
||||
|
||||
function Timestamp(props) {
|
||||
return html`
|
||||
<${SettingsContext.Consumer}>
|
||||
${(settings) => html`
|
||||
<${_Timestamp} ...${props} showSeconds=${settings.secondsInTimestamps}/>
|
||||
`}
|
||||
</>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a message can be folded.
|
||||
*
|
||||
@@ -546,7 +563,8 @@ function sameDate(d1, d2) {
|
||||
|
||||
export default class Buffer extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.buffer !== nextProps.buffer;
|
||||
return this.props.buffer !== nextProps.buffer ||
|
||||
this.props.settings !== nextProps.settings;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -557,6 +575,7 @@ export default class Buffer extends Component {
|
||||
|
||||
let server = this.props.server;
|
||||
let bouncerNetwork = this.props.bouncerNetwork;
|
||||
let settings = this.props.settings;
|
||||
let serverName = server.name;
|
||||
|
||||
let children = [];
|
||||
@@ -633,6 +652,10 @@ export default class Buffer extends Component {
|
||||
buf.messages.forEach((msg) => {
|
||||
let sep = [];
|
||||
|
||||
if (settings.bufferEvents === BufferEventsDisplayMode.HIDE && canFoldMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasUnreadSeparator && buf.type != BufferType.SERVER && !isMessageBeforeReceipt(msg, buf.prevReadReceipt)) {
|
||||
sep.push(html`<${UnreadSeparator} key="unread"/>`);
|
||||
hasUnreadSeparator = true;
|
||||
@@ -651,7 +674,7 @@ export default class Buffer extends Component {
|
||||
}
|
||||
|
||||
// TODO: consider checking the time difference too
|
||||
if (canFoldMessage(msg)) {
|
||||
if (settings.bufferEvents === BufferEventsDisplayMode.FOLD && canFoldMessage(msg)) {
|
||||
foldMessages.push(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ export default class ScrollManager extends Component {
|
||||
|
||||
restoreScrollPosition() {
|
||||
let target = this.props.target.current;
|
||||
if (!target.firstChild) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stickToKey = store.get(this.props.scrollKey);
|
||||
if (!stickToKey) {
|
||||
|
||||
112
components/settings-form.js
Normal file
112
components/settings-form.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { html, Component } from "../lib/index.js";
|
||||
|
||||
export default class SettingsForm extends Component {
|
||||
state = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state.secondsInTimestamps = props.settings.secondsInTimestamps;
|
||||
this.state.bufferEvents = props.settings.bufferEvents;
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
let target = event.target;
|
||||
let value = target.type == "checkbox" ? target.checked : target.value;
|
||||
this.setState({ [target.name]: value }, () => {
|
||||
this.props.onChange(this.state);
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
registerProtocol() {
|
||||
let url = window.location.origin + window.location.pathname + "?open=%s";
|
||||
try {
|
||||
navigator.registerProtocolHandler("irc", url);
|
||||
navigator.registerProtocolHandler("ircs", url);
|
||||
} catch (err) {
|
||||
console.error("Failed to register protocol handler: ", err);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let protocolHandler = null;
|
||||
if (this.props.showProtocolHandler) {
|
||||
protocolHandler = html`
|
||||
<div class="protocol-handler">
|
||||
<div class="left">
|
||||
Set gamja as your default IRC client for this browser.
|
||||
IRC links will be automatically opened here.
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" onClick=${() => this.registerProtocol()}>
|
||||
Enable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/><br/>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<form onChange=${this.handleChange} onSubmit=${this.handleSubmit}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secondsInTimestamps"
|
||||
checked=${this.state.secondsInTimestamps}
|
||||
/>
|
||||
Show seconds in time indicator
|
||||
</label>
|
||||
<br/><br/>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="bufferEvents"
|
||||
value="fold"
|
||||
checked=${this.state.bufferEvents === "fold"}
|
||||
/>
|
||||
Show and fold chat events
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="bufferEvents"
|
||||
value="expand"
|
||||
checked=${this.state.bufferEvents === "expand"}
|
||||
/>
|
||||
Show and expand chat events
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="bufferEvents"
|
||||
value="hide"
|
||||
checked=${this.state.bufferEvents === "hide"}
|
||||
/>
|
||||
Hide chat events
|
||||
</label>
|
||||
<br/><br/>
|
||||
|
||||
${protocolHandler}
|
||||
|
||||
<button type="button" class="danger" onClick=${() => this.props.onDisconnect()}>
|
||||
Disconnect
|
||||
</button>
|
||||
<button>
|
||||
Close
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
}
|
||||
42
lib/base64.js
Normal file
42
lib/base64.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/* The JS world is still in the stone age. We're in 2022 and we still don't
|
||||
* have the technology to correctly base64-encode a UTF-8 string. Can't wait
|
||||
* the next industrial revolution.
|
||||
*
|
||||
* For more info, see:
|
||||
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||
*/
|
||||
export function encode(data) {
|
||||
if (!window.TextEncoder) {
|
||||
return btoa(data);
|
||||
}
|
||||
|
||||
var encoder = new TextEncoder();
|
||||
var bytes = encoder.encode(data);
|
||||
|
||||
var trailing = bytes.length % 3;
|
||||
var out = "";
|
||||
for (var i = 0; i < bytes.length - trailing; i += 3) {
|
||||
var u24 = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2];
|
||||
out += alphabet[(u24 >> 18) & 0x3F];
|
||||
out += alphabet[(u24 >> 12) & 0x3F];
|
||||
out += alphabet[(u24 >> 6) & 0x3F];
|
||||
out += alphabet[u24 & 0x3F];
|
||||
}
|
||||
|
||||
if (trailing == 1) {
|
||||
var u8 = bytes[bytes.length - 1];
|
||||
out += alphabet[u8 >> 2];
|
||||
out += alphabet[(u8 << 4) & 0x3F];
|
||||
out += "==";
|
||||
} else if (trailing == 2) {
|
||||
var u16 = (bytes[bytes.length - 2] << 8) + bytes[bytes.length - 1];
|
||||
out += alphabet[u16 >> 10];
|
||||
out += alphabet[(u16 >> 4) & 0x3F];
|
||||
out += alphabet[(u16 << 2) & 0x3F];
|
||||
out += "=";
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as irc from "./irc.js";
|
||||
import * as base64 from "./base64.js";
|
||||
|
||||
// Static list of capabilities that are always requested when supported by the
|
||||
// server
|
||||
@@ -19,7 +20,6 @@ const permanentCaps = [
|
||||
|
||||
"draft/account-registration",
|
||||
"draft/chathistory",
|
||||
"draft/event-playback",
|
||||
"draft/extended-monitor",
|
||||
|
||||
"soju.im/bouncer-networks",
|
||||
@@ -124,6 +124,7 @@ export default class Client extends EventTarget {
|
||||
saslExternal: false,
|
||||
bouncerNetwork: null,
|
||||
ping: 0,
|
||||
eventPlayback: true,
|
||||
};
|
||||
debug = false;
|
||||
batches = new Map();
|
||||
@@ -460,11 +461,11 @@ export default class Client extends EventTarget {
|
||||
let initialResp = null;
|
||||
switch (mechanism) {
|
||||
case "PLAIN":
|
||||
let respStr = btoa("\0" + params.username + "\0" + params.password);
|
||||
let respStr = base64.encode("\0" + params.username + "\0" + params.password);
|
||||
initialResp = { command: "AUTHENTICATE", params: [respStr] };
|
||||
break;
|
||||
case "EXTERNAL":
|
||||
initialResp = { command: "AUTHENTICATE", params: [btoa("")] };
|
||||
initialResp = { command: "AUTHENTICATE", params: [base64.encode("")] };
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown authentication mechanism '${mechanism}'`);
|
||||
@@ -624,6 +625,9 @@ export default class Client extends EventTarget {
|
||||
if (!this.params.bouncerNetwork) {
|
||||
wantCaps.push("soju.im/bouncer-networks-notify");
|
||||
}
|
||||
if (this.params.eventPlayback) {
|
||||
wantCaps.push("draft/event-playback");
|
||||
}
|
||||
|
||||
let msg = this.caps.requestAvailable(wantCaps);
|
||||
if (msg) {
|
||||
@@ -886,7 +890,7 @@ export default class Client extends EventTarget {
|
||||
let max = Math.min(limit, this.isupport.chatHistory());
|
||||
let params = ["BEFORE", target, "timestamp=" + before, max];
|
||||
return this.roundtripChatHistory(params).then((messages) => {
|
||||
return { more: messages.length >= max };
|
||||
return { messages, more: messages.length >= max };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -904,7 +908,7 @@ export default class Client extends EventTarget {
|
||||
after.time = messages[messages.length - 1].tags.time;
|
||||
return this.fetchHistoryBetween(target, after, before, limit);
|
||||
}
|
||||
return null;
|
||||
return { messages };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -474,6 +474,10 @@ export class Isupport {
|
||||
}
|
||||
return chanModes;
|
||||
}
|
||||
|
||||
bot() {
|
||||
return this.raw.get("BOT");
|
||||
}
|
||||
}
|
||||
|
||||
export const CaseMapping = {
|
||||
|
||||
13604
package-lock.json
generated
13604
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
state.js
17
state.js
@@ -1,5 +1,6 @@
|
||||
import * as irc from "./lib/irc.js";
|
||||
import Client from "./lib/client.js";
|
||||
import { createContext } from "./lib/index.js";
|
||||
|
||||
export const SERVER_BUFFER = "*";
|
||||
|
||||
@@ -34,6 +35,14 @@ export const ReceiptType = {
|
||||
READ: "read",
|
||||
};
|
||||
|
||||
export const BufferEventsDisplayMode = {
|
||||
FOLD: "fold",
|
||||
EXPAND: "expand",
|
||||
HIDE: "hide",
|
||||
};
|
||||
|
||||
export const SettingsContext = createContext("settings");
|
||||
|
||||
export function getNickURL(nick) {
|
||||
return "irc:///" + encodeURIComponent(nick) + ",isuser";
|
||||
}
|
||||
@@ -209,6 +218,10 @@ export const State = {
|
||||
buffers: new Map(),
|
||||
activeBuffer: null,
|
||||
bouncerNetworks: new Map(),
|
||||
settings: {
|
||||
secondsInTimestamps: true,
|
||||
bufferEvents: BufferEventsDisplayMode.FOLD,
|
||||
},
|
||||
};
|
||||
},
|
||||
updateServer(state, id, updater) {
|
||||
@@ -454,6 +467,10 @@ export const State = {
|
||||
if (who.flags !== undefined) {
|
||||
who.away = who.flags.indexOf("G") >= 0; // H for here, G for gone
|
||||
who.operator = who.flags.indexOf("*") >= 0;
|
||||
let botFlag = client.isupport.bot();
|
||||
if (botFlag) {
|
||||
who.bot = who.flags.indexOf(botFlag) >= 0;
|
||||
}
|
||||
delete who.flags;
|
||||
}
|
||||
|
||||
|
||||
6
store.js
6
store.js
@@ -26,6 +26,7 @@ class Item {
|
||||
|
||||
export const autoconnect = new Item("autoconnect");
|
||||
export const naggedProtocolHandler = new Item("naggedProtocolHandler");
|
||||
export const settings = new Item("settings");
|
||||
|
||||
function debounce(f, delay) {
|
||||
let timeout = null;
|
||||
@@ -57,11 +58,12 @@ export class Buffer {
|
||||
}
|
||||
|
||||
key(buf) {
|
||||
// TODO: use case-mapping here somehow
|
||||
return JSON.stringify({
|
||||
name: buf.name,
|
||||
name: buf.name.toLowerCase(),
|
||||
server: {
|
||||
url: buf.server.url,
|
||||
nick: buf.server.nick,
|
||||
nick: buf.server.nick.toLowerCase(),
|
||||
bouncerNetwork: buf.server.bouncerNetwork,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -533,6 +533,14 @@ details summary[role="button"] {
|
||||
overflow: auto; /* hack to clear floating elements */
|
||||
}
|
||||
|
||||
.dialog .protocol-handler {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.dialog .protocol-handler .left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #bfbfbf;
|
||||
|
||||
Reference in New Issue
Block a user