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[] = []; 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); } } // also update any boosts by this author case "thread": case "profile": break; } } } break; } } setIdxView() { this.innerHTML = "" } setThreadView() { let html = `← return to index`; html += ""; 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() { // populate boost carousel // skip dm list for now // public/unified list const pl = util.$("public_list"); if (pl) { let html = ""; for (const t of this._threads.sort((a: MessageThread, b: MessageThread) => b.latest - a.latest)) { html +=`
  • `; } 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 { // 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 = ``; 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 // 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; }, 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; } 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; } for (let t of this._threads) { // avoid processing nodes again on subsequent passes if (!msg || t.findNode(t.root, msg.id)) { return null; } 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; } } } // 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); } } return msg.id; } // 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); } } // 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); } } return null; } }