underbbs/frontend/ts/adapter-element.ts

285 lines
9.2 KiB
TypeScript
Raw Normal View History

import util from "./util"
import { Message, Author } from "./message"
import { MessageThread } from "./thread"
import { AdapterState } from "./adapter"
import { BatchTimer } from "./batch-timer"
export class AdapterElement extends HTMLElement {
static observedAttributes = [ "data-latest", "data-view", "data-viewing" ]
private _latest: string = "" ;
private _view: string = "";
private _name: string = ""
private _viewing: string = "";
private _convoyBatchTimer = new BatchTimer((ids: string[])=>{
let url = `/api/adapters/${this._name}/fetch?entity_type=convoy`;
for (let id of ids) {
url += `&entity_id=${id}`;
}
util.authorizedFetch("GET", url, null)
});
// TODO: use visibility of the thread to organize into DMs and public threads
private _threads: MessageThread[] = [];
private _orphans: Message[] = [];
2024-08-31 17:01:31 +00:00
private _boosts: Message[] = [];
constructor() {
super();
}
connectedCallback() {
const name = this.getAttribute("data-name");
this._name = name ?? "";
this.buildThreads();
}
attributeChangedCallback(attr: string, prev: string, next: string) {
switch (attr) {
case "data-viewing":
if (next != prev) {
this._viewing = next;
}
// probably only needed for profile view; we might be able to move this to child components
// keep it simple for now, but eventually we will need a way to force _view to update
break;
case "data-view":
if (!next) {
this._view = "index";
} else {
this._view = next;
}
switch (this._view) {
case "index":
this.setIdxView();
this.populateIdxView();
break;
case "thread":
this.setThreadView();
this.populateThreadView();
break;
case "profile":
this.setProfileView();
this.populateProfileView();
break;
}
break;
case "data-latest":
let datastore = AdapterState._instance.data.get(this._name);
if (!datastore) {
// this shouldn't be possible
return;
}
if (prev != next && next) {
this._latest = next;
}
const latestMsg = datastore.messages.get(this._latest);
if (latestMsg) {
const rootId = this.placeMsg(this._latest);
// if rootId is null, this is an orphan and we don't need to actually do any UI updates yet
if (rootId) {
switch (this._view) {
case "index":
this.updateIdxView(this._latest, rootId);
break;
case "thread":
// if the the message is part of this thread, update it
case "profile":
// if the message is from this user, show it in their profile
break;
}
}
} else {
const latestAuthor = datastore.profileCache.get(this._latest);
if (latestAuthor) {
switch (this._view) {
case "index":
const threadsByThisAuthor = this._threads.filter(t=>t.root.data.author == this._latest);
for (let t of threadsByThisAuthor) {
let tse = this.querySelector(`underbbs-thread-summary[data-msg='${t.root.data.id}']`)
if (tse) {
tse.setAttribute("data-author", this._latest);
}
}
2024-08-31 17:01:31 +00:00
// also update any boosts by this author
case "thread":
case "profile":
break;
}
}
}
break;
}
}
setIdxView() {
2024-08-31 17:01:31 +00:00
this.innerHTML = "<ul id='boost_carousel'></ul><ul id='dm_list'></ul><ul id='public_list'></ul>"
}
setThreadView() {
let html = `<a href="#${this._name}">&larr; return to index</a>`;
html += "<ul id='msg_list'></ul>";
this.innerHTML = html;
}
setProfileView() {
let profile_bar = util.$("profile_bar");
if (profile_bar) {
// clear any previous data
} else {
// insert the profileSidebar into the dom
}
}
populateIdxView() {
2024-08-31 17:01:31 +00:00
// populate boost carousel
// skip dm list for now
// public/unified list
const pl = util.$("public_list");
if (pl) {
2024-07-05 05:23:56 +00:00
let html = "";
for (const t of this._threads.sort((a: MessageThread, b: MessageThread) => b.latest - a.latest)) {
html +=`<li><underbbs-thread-summary data-len="${t.messageCount}" data-adapter="${t.root.data.adapter}" data-msg="${t.root.data.id}" data-created="${t.created}" data-latest="${t.latest}"></underbbs-thread-summary></li>`;
}
2024-07-05 05:23:56 +00:00
pl.innerHTML = html;
}
}
updateIdxView(latest: string, rootId: string) {
const threadSelector = `underbbs-thread-summary[data-msg='${rootId}']`
const existingThread = document.querySelector(threadSelector);
const thread = this._threads.find(t=>t.root.data.id == rootId);
if (existingThread && thread) {
existingThread.setAttribute("data-latest", `${thread.latest}`);
existingThread.setAttribute("data-len", `${thread.messageCount}`);
existingThread.setAttribute("data-new", "true");
} else {
2024-08-31 17:01:31 +00:00
// if latest is a boost, put it in the carousel
// unified/public list for now
const pl = util.$("public_list");
if (pl && thread) {
const li = document.createElement("li");
li.innerHTML = `<underbbs-thread-summary data-len="1" data-adapter="${thread.root.data.adapter}" data-msg="${thread.root.data.id}" data-latest="${thread.latest}" data-created="${thread.created}"></underbbs-thread-summary>`;
let nextThread: Element | null = null;
for (let i = 0; i < pl.children.length; i++) {
const c = pl.children.item(i);
const latest = c?.children.item(0)?.getAttribute("data-latest")
if (latest && parseInt(latest) < thread.latest) {
nextThread = c;
break;
}
}
if (nextThread) {
nextThread.insertAdjacentElement('beforebegin', li);
} else {
pl.append(li);
}
}
}
}
populateThreadView() {
}
populateProfileView() {
}
buildThreads() {
const datastore = AdapterState._instance.data.get(this._name);
if (!datastore) {
util.errMsg(this._name + " has no datastore!");
return;
}
// make multiple passes over the store until every message is either
2024-08-31 17:01:31 +00:00
// placed in a thread, the boost carousel, or orphaned and waiting for its parent to be returned
do{
for (let k of datastore.messages.keys()) {
this.placeMsg(k);
}
} while (this._threads.reduce((sum: number, thread: MessageThread)=>{
return sum + thread.messageCount;
2024-08-31 17:01:31 +00:00
}, 0) + this._boosts.length + this._orphans.length < datastore.messages.size);
}
placeMsg(k: string): string | null {
const datastore = AdapterState._instance.data.get(this._name);
if (!datastore) {
util.errMsg(this._name + " has no datastore!");
return null;
}
const msg = datastore.messages.get(k);
if (!msg) {
util.errMsg(`message [${this._name}:${k}] doesn't exist`);
return null;
}
2024-08-31 17:01:31 +00:00
if (msg.renoteId) {
// fetch the referent thread and put the boost in the carousel
this._convoyBatchTimer.queue(msg.renoteId, 2000);
if (!this._boosts.some(m=>m.id == msg.id)) {
this._boosts.push(msg);
}
return null;
}
2024-07-05 05:23:56 +00:00
for (let t of this._threads) {
// avoid processing nodes again on subsequent passes
if (!msg || t.findNode(t.root, msg.id)) {
2024-07-05 05:23:56 +00:00
return null;
}
2024-08-31 17:01:31 +00:00
2024-07-05 05:23:56 +00:00
if (msg.replyTo) {
let x = t.addReply(msg.replyTo, msg);
if (x) {
// after adding, we try to adopt some orphans
const orphanChildren = this._orphans.filter(m=>m.replyTo == k);
for (let o of orphanChildren) {
let adopted = this.placeMsg(o.id);
if (adopted) {
this._orphans.splice(this._orphans.indexOf(o), 1);
}
}
return t.root.data.id;
2024-07-05 05:23:56 +00:00
}
}
}
// if we made it this far, this message doesn't go in any existing thread
// if it doesn't have a parent, we can make a new thread with it
if (!msg.replyTo) {
this._threads.push(new MessageThread(msg));
// after adding, we try to adopt some orphans
const orphanChildren = this._orphans.filter(m=>m.replyTo == k);
for (let o of orphanChildren) {
let adopted = this.placeMsg(o.id);
if (adopted) {
this._orphans.splice(this._orphans.indexOf(o), 1);
}
}
2024-07-05 05:23:56 +00:00
return msg.id;
}
2024-07-06 18:43:25 +00:00
// then, we should check if its parent is an orphan
const orphanedParent = this._orphans.find(o=>o.id == msg.replyTo);
if (orphanedParent) {
// then, try to place them both
if (this.placeMsg(orphanedParent.id)) {
this._orphans.splice(this._orphans.indexOf(orphanedParent), 1);
return this.placeMsg(k);
2024-07-06 18:43:25 +00:00
}
}
2024-07-05 05:23:56 +00:00
// otherwise we can orphan it and try to fill it in later
if (this._orphans.filter(o=>o.id == msg.id).length == 0) {
this._orphans.push(msg);
if (msg.replyTo) {
this._convoyBatchTimer.queue(k, 2000);
}
}
2024-07-05 05:23:56 +00:00
return null;
}
}