Implement chathistory support

This commit is contained in:
Simon Ser 2020-06-29 09:06:47 +02:00
parent 8809fdcd6a
commit c9b07efc9c
No known key found for this signature in database
GPG key ID: 0FDE7BE0E88F5E48
4 changed files with 136 additions and 16 deletions

View file

@ -11,6 +11,7 @@ import { html, Component, createRef } from "/lib/index.js";
import { BufferType, Status, Unread } from "/state.js";
const SERVER_BUFFER = "*";
const CHATHISTORY_PAGE_SIZE = 100;
var messagesCount = 0;
@ -27,6 +28,29 @@ function parseQueryString() {
return params;
}
/* Insert a message in an immutable list of sorted messages. */
function insertMessage(list, msg) {
if (list.length == 0) {
return [msg];
} else if (list[list.length - 1].tags.time <= msg.tags.time) {
return list.concat(msg);
}
var insertBefore = -1;
for (var i = 0; i < list.length; i++) {
var other = list[i];
if (msg.tags.time < other.tags.time) {
insertBefore = i;
break;
}
}
console.assert(insertBefore >= 0, "");
list = [ ...list ];
list.splice(insertBefore, 0, msg);
return list;
}
export default class App extends Component {
client = null;
state = {
@ -44,6 +68,7 @@ export default class App extends Component {
buffers: new Map(),
activeBuffer: null,
};
endOfHistory = new Map();
buffer = createRef();
composer = createRef();
@ -56,6 +81,7 @@ export default class App extends Component {
this.handleNickClick = this.handleNickClick.bind(this);
this.handleJoinClick = this.handleJoinClick.bind(this);
this.autocomplete = this.autocomplete.bind(this);
this.handleBufferScrollTop = this.handleBufferScrollTop.bind(this);
if (window.localStorage && localStorage.getItem("autoconnect")) {
var connectParams = JSON.parse(localStorage.getItem("autoconnect"));
@ -161,21 +187,18 @@ export default class App extends Component {
if (!msg.tags) {
msg.tags = {};
}
if (!msg.tags["time"]) {
// Format the current time according to ISO 8601
var date = new Date();
var YYYY = date.getUTCFullYear().toString().padStart(4, "0");
var MM = (date.getUTCMonth() + 1).toString().padStart(2, "0");
var DD = date.getUTCDate().toString().padStart(2, "0");
var hh = date.getUTCHours().toString().padStart(2, "0");
var mm = date.getUTCMinutes().toString().padStart(2, "0");
var ss = date.getUTCSeconds().toString().padStart(2, "0");
var sss = date.getUTCMilliseconds().toString().padStart(3, "0");
msg.tags["time"] = `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${sss}Z`;
if (!msg.tags.time) {
msg.tags.time = irc.formatDate(new Date());
}
var isHistory = false;
if (msg.tags.batch && this.client.batches.has(msg.tags.batch)) {
var batch = this.client.batches.get(msg.tags.batch);
isHistory = batch.type == "chathistory";
}
var msgUnread = Unread.NONE;
if (msg.command == "PRIVMSG" || msg.command == "NOTICE") {
if ((msg.command == "PRIVMSG" || msg.command == "NOTICE") && !isHistory) {
var target = msg.params[0];
var text = msg.params[1];
@ -215,8 +238,9 @@ export default class App extends Component {
if (state.activeBuffer != buf.name) {
unread = Unread.union(unread, msgUnread);
}
var messages = insertMessage(buf.messages, msg);
return {
messages: buf.messages.concat(msg),
messages,
unread,
};
});
@ -394,6 +418,18 @@ export default class App extends Component {
return { who };
});
break;
case "BATCH":
var enter = msg.params[0].startsWith("+");
var name = msg.params[0].slice(1);
if (enter) {
break;
}
var batch = this.client.batches.get(name);
if (batch.type == "chathistory" && batch.messages.length < CHATHISTORY_PAGE_SIZE) {
var target = batch.params[0];
this.endOfHistory.set(target, true);
}
break;
case "CAP":
case "AUTHENTICATE":
case "PING":
@ -612,6 +648,32 @@ export default class App extends Component {
return repl;
}
handleBufferScrollTop() {
var target = this.state.activeBuffer;
if (!target || target == SERVER_BUFFER) {
return;
}
if (!this.client.enabledCaps["draft/chathistory"] || !this.client.enabledCaps["server-time"]) {
return;
}
if (this.endOfHistory.has(target)) {
return;
}
var buf = this.state.buffers.get(target);
var before;
if (buf.messages.length > 0) {
before = buf.messages[0].tags["time"];
} else {
before = irc.formatDate(new Date());
}
this.client.send({
command: "CHATHISTORY",
params: ["BEFORE", target, "timestamp=" + before, CHATHISTORY_PAGE_SIZE],
});
}
componentDidMount() {
if (this.state.connectParams.autoconnect) {
this.connect(this.state.connectParams);
@ -661,7 +723,7 @@ export default class App extends Component {
</div>
</section>
${bufferHeader}
<${ScrollManager} target=${this.buffer} scrollKey=${this.state.activeBuffer}>
<${ScrollManager} target=${this.buffer} scrollKey=${this.state.activeBuffer} onScrollTop=${this.handleBufferScrollTop}>
<section id="buffer" ref=${this.buffer}>
<${Buffer} buffer=${activeBuffer} onNickClick=${this.handleNickClick}/>
</section>

View file

@ -40,10 +40,16 @@ export default class ScrollManager extends Component {
}
this.scroll(pos);
this.stickToBottom = pos.bottom;
if (this.props.target.current.scrollTop == 0) {
this.props.onScrollTop();
}
}
handleScroll() {
this.stickToBottom = this.isAtBottom();
if (this.props.target.current.scrollTop == 0) {
this.props.onScrollTop();
}
}
componentDidMount() {

View file

@ -2,7 +2,15 @@ import * as irc from "./irc.js";
// Static list of capabilities that are always requested when supported by the
// server
const permanentCaps = ["message-tags", "server-time", "multi-prefix", "away-notify", "echo-message"];
const permanentCaps = [
"away-notify",
"batch",
"draft/chathistory",
"echo-message",
"message-tags",
"multi-prefix",
"server-time",
];
export default class Client extends EventTarget {
ws = null;
@ -18,6 +26,7 @@ export default class Client extends EventTarget {
registered = false;
availableCaps = {};
enabledCaps = {};
batches = new Map();
constructor(params) {
super();
@ -65,6 +74,15 @@ export default class Client extends EventTarget {
var msg = irc.parseMessage(event.data);
console.log("Received:", msg);
var msgBatch = null;
if (msg.tags["batch"]) {
msgBatch = this.batches.get(msg.tags["batch"]);
if (msgBatch) {
msgBatch.messages.push(msg);
}
}
var deleteBatch = null;
switch (msg.command) {
case irc.RPL_WELCOME:
if (this.params.saslPlain && this.availableCaps["sasl"] === undefined) {
@ -115,11 +133,33 @@ export default class Client extends EventTarget {
this.nick = newNick;
}
break;
case "BATCH":
var enter = msg.params[0].startsWith("+");
var name = msg.params[0].slice(1);
if (enter) {
var batch = {
name,
type: msg.params[1],
params: msg.params.slice(2),
parent: msgBatch,
messages: [],
};
this.batches.set(name, batch);
} else {
deleteBatch = name;
}
break;
}
this.dispatchEvent(new CustomEvent("message", {
detail: { message: msg },
detail: { message: msg, batch: msgBatch },
}));
// Delete after firing the message event so that handlers can access
// the batch
if (deleteBatch) {
this.batches.delete(name);
}
}
addAvailableCaps(s) {

View file

@ -256,3 +256,15 @@ export function isError(cmd) {
return false;
}
}
export function formatDate(date) {
// ISO 8601
var YYYY = date.getUTCFullYear().toString().padStart(4, "0");
var MM = (date.getUTCMonth() + 1).toString().padStart(2, "0");
var DD = date.getUTCDate().toString().padStart(2, "0");
var hh = date.getUTCHours().toString().padStart(2, "0");
var mm = date.getUTCMinutes().toString().padStart(2, "0");
var ss = date.getUTCSeconds().toString().padStart(2, "0");
var sss = date.getUTCMilliseconds().toString().padStart(3, "0");
return `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${sss}Z`;
}