Implement chathistory support
This commit is contained in:
+76
-14
@@ -11,6 +11,7 @@ import { html, Component, createRef } from "/lib/index.js";
|
|||||||
import { BufferType, Status, Unread } from "/state.js";
|
import { BufferType, Status, Unread } from "/state.js";
|
||||||
|
|
||||||
const SERVER_BUFFER = "*";
|
const SERVER_BUFFER = "*";
|
||||||
|
const CHATHISTORY_PAGE_SIZE = 100;
|
||||||
|
|
||||||
var messagesCount = 0;
|
var messagesCount = 0;
|
||||||
|
|
||||||
@@ -27,6 +28,29 @@ function parseQueryString() {
|
|||||||
return params;
|
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 {
|
export default class App extends Component {
|
||||||
client = null;
|
client = null;
|
||||||
state = {
|
state = {
|
||||||
@@ -44,6 +68,7 @@ export default class App extends Component {
|
|||||||
buffers: new Map(),
|
buffers: new Map(),
|
||||||
activeBuffer: null,
|
activeBuffer: null,
|
||||||
};
|
};
|
||||||
|
endOfHistory = new Map();
|
||||||
buffer = createRef();
|
buffer = createRef();
|
||||||
composer = createRef();
|
composer = createRef();
|
||||||
|
|
||||||
@@ -56,6 +81,7 @@ export default class App extends Component {
|
|||||||
this.handleNickClick = this.handleNickClick.bind(this);
|
this.handleNickClick = this.handleNickClick.bind(this);
|
||||||
this.handleJoinClick = this.handleJoinClick.bind(this);
|
this.handleJoinClick = this.handleJoinClick.bind(this);
|
||||||
this.autocomplete = this.autocomplete.bind(this);
|
this.autocomplete = this.autocomplete.bind(this);
|
||||||
|
this.handleBufferScrollTop = this.handleBufferScrollTop.bind(this);
|
||||||
|
|
||||||
if (window.localStorage && localStorage.getItem("autoconnect")) {
|
if (window.localStorage && localStorage.getItem("autoconnect")) {
|
||||||
var connectParams = JSON.parse(localStorage.getItem("autoconnect"));
|
var connectParams = JSON.parse(localStorage.getItem("autoconnect"));
|
||||||
@@ -161,21 +187,18 @@ export default class App extends Component {
|
|||||||
if (!msg.tags) {
|
if (!msg.tags) {
|
||||||
msg.tags = {};
|
msg.tags = {};
|
||||||
}
|
}
|
||||||
if (!msg.tags["time"]) {
|
if (!msg.tags.time) {
|
||||||
// Format the current time according to ISO 8601
|
msg.tags.time = irc.formatDate(new Date());
|
||||||
var date = new Date();
|
}
|
||||||
var YYYY = date.getUTCFullYear().toString().padStart(4, "0");
|
|
||||||
var MM = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
var isHistory = false;
|
||||||
var DD = date.getUTCDate().toString().padStart(2, "0");
|
if (msg.tags.batch && this.client.batches.has(msg.tags.batch)) {
|
||||||
var hh = date.getUTCHours().toString().padStart(2, "0");
|
var batch = this.client.batches.get(msg.tags.batch);
|
||||||
var mm = date.getUTCMinutes().toString().padStart(2, "0");
|
isHistory = batch.type == "chathistory";
|
||||||
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`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var msgUnread = Unread.NONE;
|
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 target = msg.params[0];
|
||||||
var text = msg.params[1];
|
var text = msg.params[1];
|
||||||
|
|
||||||
@@ -215,8 +238,9 @@ export default class App extends Component {
|
|||||||
if (state.activeBuffer != buf.name) {
|
if (state.activeBuffer != buf.name) {
|
||||||
unread = Unread.union(unread, msgUnread);
|
unread = Unread.union(unread, msgUnread);
|
||||||
}
|
}
|
||||||
|
var messages = insertMessage(buf.messages, msg);
|
||||||
return {
|
return {
|
||||||
messages: buf.messages.concat(msg),
|
messages,
|
||||||
unread,
|
unread,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -394,6 +418,18 @@ export default class App extends Component {
|
|||||||
return { who };
|
return { who };
|
||||||
});
|
});
|
||||||
break;
|
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 "CAP":
|
||||||
case "AUTHENTICATE":
|
case "AUTHENTICATE":
|
||||||
case "PING":
|
case "PING":
|
||||||
@@ -612,6 +648,32 @@ export default class App extends Component {
|
|||||||
return repl;
|
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() {
|
componentDidMount() {
|
||||||
if (this.state.connectParams.autoconnect) {
|
if (this.state.connectParams.autoconnect) {
|
||||||
this.connect(this.state.connectParams);
|
this.connect(this.state.connectParams);
|
||||||
@@ -661,7 +723,7 @@ export default class App extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
${bufferHeader}
|
${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}>
|
<section id="buffer" ref=${this.buffer}>
|
||||||
<${Buffer} buffer=${activeBuffer} onNickClick=${this.handleNickClick}/>
|
<${Buffer} buffer=${activeBuffer} onNickClick=${this.handleNickClick}/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -40,10 +40,16 @@ export default class ScrollManager extends Component {
|
|||||||
}
|
}
|
||||||
this.scroll(pos);
|
this.scroll(pos);
|
||||||
this.stickToBottom = pos.bottom;
|
this.stickToBottom = pos.bottom;
|
||||||
|
if (this.props.target.current.scrollTop == 0) {
|
||||||
|
this.props.onScrollTop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleScroll() {
|
handleScroll() {
|
||||||
this.stickToBottom = this.isAtBottom();
|
this.stickToBottom = this.isAtBottom();
|
||||||
|
if (this.props.target.current.scrollTop == 0) {
|
||||||
|
this.props.onScrollTop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|||||||
+42
-2
@@ -2,7 +2,15 @@ import * as irc from "./irc.js";
|
|||||||
|
|
||||||
// Static list of capabilities that are always requested when supported by the
|
// Static list of capabilities that are always requested when supported by the
|
||||||
// server
|
// 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 {
|
export default class Client extends EventTarget {
|
||||||
ws = null;
|
ws = null;
|
||||||
@@ -18,6 +26,7 @@ export default class Client extends EventTarget {
|
|||||||
registered = false;
|
registered = false;
|
||||||
availableCaps = {};
|
availableCaps = {};
|
||||||
enabledCaps = {};
|
enabledCaps = {};
|
||||||
|
batches = new Map();
|
||||||
|
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
super();
|
super();
|
||||||
@@ -65,6 +74,15 @@ export default class Client extends EventTarget {
|
|||||||
var msg = irc.parseMessage(event.data);
|
var msg = irc.parseMessage(event.data);
|
||||||
console.log("Received:", msg);
|
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) {
|
switch (msg.command) {
|
||||||
case irc.RPL_WELCOME:
|
case irc.RPL_WELCOME:
|
||||||
if (this.params.saslPlain && this.availableCaps["sasl"] === undefined) {
|
if (this.params.saslPlain && this.availableCaps["sasl"] === undefined) {
|
||||||
@@ -115,11 +133,33 @@ export default class Client extends EventTarget {
|
|||||||
this.nick = newNick;
|
this.nick = newNick;
|
||||||
}
|
}
|
||||||
break;
|
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", {
|
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) {
|
addAvailableCaps(s) {
|
||||||
|
|||||||
+12
@@ -256,3 +256,15 @@ export function isError(cmd) {
|
|||||||
return false;
|
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`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user