import util from "./util" import { Message, Author } from "./message" import { MessageThread } from "./thread" import { AdapterState } from "./adapter" 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 = ""; // TODO: use visibility of the thread to organize into DMs and public threads private _threads: MessageThread[] = []; private _orphans: Message[] = []; constructor() { super(); } connectedCallback() { const name = this.getAttribute("data-name"); this._name = name ?? ""; this._view = ""; this.buildThreads(); this.setAttribute("data-view", "index"); } attributeChangedCallback() { console.log(`${this._name}.attributeChangedCallback: start`); // set the viewing subject if it's changed const viewing = this.getAttribute("data-viewing"); if (this._viewing != viewing && viewing != null) { console.log(`${this._name}.attributeChangedCallback: resetting viewing subject`); this._viewing = viewing; // if the viewing subject changed (not to nothing), unset the view // this will force it to refresh if (this._viewing) { console.log(`${this._name}.attributeChangedCallback: forcing view update`); this._view = ""; } } // initialize the view if it's changed const view = this.getAttribute("data-view"); if (this._view != view ?? "index") { this._view = view ?? "index"; console.log(`${this._name}.attributeChangedCallback: setting view: ${this._view}`); switch (this._view) { case "index": this.setIdxView(); this.populateIdxView(); break; case "thread": this.setThreadView(); this.populateThreadView(); break; case "profile": this.setProfileView(); this.populateProfileView(); break; } } // if latest changed, check if it's a message const latest = this.getAttribute("data-latest"); console.log(`${this._name}.attributeChangedCallback: checking latest(${latest}) vs _latest${this._latest}`); if (latest ?? "" != this._latest) { console.log("latest changed") this._latest = latest ?? ""; let datastore = AdapterState._instance.data.get(this._name); if (!datastore) { util.errMsg(this._name + " has no datastore!"); return; } const latestMsg = datastore.messages.get(this._latest); if (latestMsg) { console.log('latest was a message; place it'); const rootId = this.placeMsg(this._latest); // if rootId is null, this is an orphan and we don't need to actually do any updates yet if (rootId) { switch (this._view) { case "index": console.log(`message was placed in thread ${rootId}, update view`) 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": console.log (`author was updated: ${this._latest}, update their threads`) 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) { console.log(`author has a thread in the dom, update it: ${t.root.data.id}`) tse.setAttribute("data-author", this._latest); } } case "thread": case "profile": break; } } } // so, try to insert it into the threads // then, switch on view // if index, iterate through the topics and find the one to indicate new activity, // if thread, if any relatives are in this thread, insert message appropriately // if profile, if latest is this profile, update it } } 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() { // 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 existingThread = this.querySelector(`underbbs-thread-summary[data-msg="${rootId}"]`); const thread = this._threads.find(t=>t.root.data.id == rootId); if (existingThread && thread) { console.log(`updating thread: ${thread.root.data.id} // ${thread.messageCount} NEW`) existingThread.setAttribute("data-latest", `${thread.latest}`); existingThread.setAttribute("data-len", `${thread.messageCount}`); existingThread.setAttribute("data-new", "true"); } else { // 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) return } 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, 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._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; } 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) { // request the parent's data, which will try to adopt this orphan when it comes in util.authorizedFetch( "GET", `/api/adapters/${this._name}/fetch?entity_type=message&entity_id=${msg.replyTo}`, null); } } return null; } }