mirror of
https://codeberg.org/emersion/gamja.git
synced 2024-11-14 19:05:01 -05:00
Implement chathistory support
This commit is contained in:
parent
8809fdcd6a
commit
c9b07efc9c
4 changed files with 136 additions and 16 deletions
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
12
lib/irc.js
12
lib/irc.js
|
@ -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`;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue