all repos — underbbs @ 2976d438d90bea2beb4e67272fd628ff163459f2

decentralized social media client

use static classes for Settings and AdapterState
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iHUEABYKAB0WIQT/foVVmI9pK13hPWFohAcXSWbK8wUCZpbNZwAKCRBohAcXSWbK
89uyAP9fyPleHBtmmqC6+llfcVM5UShzklpLm1Ytk8nCcHXvzQD+OJ42xy20Tc4K
Y8ZSYhfityKgfIFlqK0mwguJ2tg0FgA=
=H4QM
-----END PGP SIGNATURE-----
commit

2976d438d90bea2beb4e67272fd628ff163459f2

parent

fead16168a5af4d28606cc1a7cef0b23ce85ed6c

M README.mdREADME.md

@@ -10,11 +10,17 @@ each distinct `adapter` connection/configuration is represented in the frontend as a tab, and using the websocket's event-driven javascript interface with web components we can simply either store the data or tell the currently visible adapter that it might need to respond to the new data

adapters receive commands via a quartzgun web API and send data back on their shared websocket connection -## building +## building and running requirements are - go 1.22 - any recent nodejs that can do `typescript` and `webpack` 5 -run `./build.sh` from the project root. you can supply 'front' or 'server' as an argument to build only one or the other; by default it builds both+from the project root: + +1. `./build.sh front` +2. `./build.sh server` +3. `./underbbs` + +visit `http://localhost:9090/app`
M adapter/mastodon.goadapter/mastodon.go

@@ -110,10 +110,10 @@ Adapter: self.nickname,

Id: fmt.Sprintf("%d", status.ID), Uri: status.URI, Type: "message", + Created: status.CreatedAt.UnixMilli(), }, Content: status.Content, Author: status.Account.Acct, - Created: status.CreatedAt, Visibility: status.Visibility, } if status.InReplyToID != nil {
M adapter/misskey.goadapter/misskey.go

@@ -205,9 +205,8 @@ Uri: n.URI,

Protocol: "misskey", Adapter: self.nickname, Type: "message", + Created: n.CreatedAt.UnixMilli(), }, - - Created: n.CreatedAt, Author: authorId, Content: n.Text,

@@ -224,7 +223,7 @@ Src: f.URL,

ThumbSrc: f.ThumbnailURL, Size: f.Size, Desc: f.Comment, - CreatedAt: f.CreatedAt, + Created: f.CreatedAt.UnixMilli(), }) } return &msg

@@ -242,6 +241,12 @@ } else {

authorId = fmt.Sprintf("@%s", usr.Username) } + var updated *int64 = nil + if usr.UpdatedAt != nil { + updatedTmp := usr.UpdatedAt.UnixMilli() + updated = &updatedTmp + } + author := Author{ Datagram: Datagram{ Id: authorId,

@@ -249,6 +254,8 @@ Uri: mkcore.StringValue(usr.URL),

Protocol: "misskey", Adapter: self.nickname, Type: "author", + Created: usr.CreatedAt.UnixMilli(), + Updated: updated, }, Name: usr.Name, ProfilePic: usr.AvatarURL,

@@ -303,7 +310,6 @@ }

case "author": user := "" host := "" - fmt.Printf("fetch author: %s\n", id) idParts := strings.Split(id, "@") user = idParts[1] if len(idParts) == 3 {

@@ -313,12 +319,6 @@

var hostPtr *string = nil if len(host) > 0 { hostPtr = &host - } - - if hostPtr == nil { - fmt.Printf("looking up user: @%s\n", user) - } else { - fmt.Printf("looking up remote user: @%s@%s\n", user, host) } // fmt.Printf("attempting user resolution: @%s@%s\n", user, host)
M adapter/nostr.goadapter/nostr.go

@@ -106,7 +106,7 @@ switch evt.Kind {

case nostr.KindTextNote: m.Id = evt.ID m.Author = evt.PubKey - m.Created = evt.CreatedAt.Time() + m.Created = evt.CreatedAt.Time().UnixMilli() m.Content = evt.Content return m, nil default:
M build.shbuild.sh

@@ -23,7 +23,6 @@ go mod tidy

go build ;; *) - $0 client - $0 server + echo "usage: ${0} <front|server>" ;; esac
M frontend/ts/adapter-element.tsfrontend/ts/adapter-element.ts

@@ -1,9 +1,8 @@

import util from "./util" + import { Message, Author } from "./message" import { MessageThread } from "./thread" - -var _ = util._ -var $ = util.$ +import { AdapterState } from "./adapter" export class AdapterElement extends HTMLElement { static observedAttributes = [ "data-latest", "data-view", "data-viewing" ]

@@ -30,14 +29,17 @@ 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 = ""; } }

@@ -46,6 +48,8 @@ // 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();

@@ -63,18 +67,25 @@ }

} // if latest changed, check if it's a message - const latest = this.getAttribute("latest"); + 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 = _("datastore"); - console.log(datastore); + 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":

@@ -85,12 +96,23 @@ break;

} } } else { - const latestAuthor = _("datastore")[this._name].profileCache.get(this._latest); - switch (this._view) { - case "index": - case "thread": - case "profile": - break; + 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

@@ -113,7 +135,7 @@ this.innerHTML = html;

} setProfileView() { - let profile_bar = $("profile_bar"); + let profile_bar = util.$("profile_bar"); if (profile_bar) { // clear any previous data } else {

@@ -124,28 +146,44 @@

populateIdxView() { // skip dm list for now // public/unified list - const pl = $("public_list"); + const pl = util.$("public_list"); if (pl) { let html = ""; - for (const t of this._threads) { - 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}"></underbbs-thread-summary></li>`; + 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>`; } pl.innerHTML = html; } } updateIdxView(latest: string, rootId: string) { - const existingThread = this.querySelector("underbbs-thread-summary[msg='${this._latest}']"); + const existingThread = this.querySelector(`underbbs-thread-summary[data-msg="${rootId}"]`); const thread = this._threads.find(t=>t.root.data.id == rootId); if (existingThread && thread) { - existingThread.setAttribute("data-latest", `${thread.latest[Symbol.toPrimitive]("number")}`); + 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 = $("public_list"); + const pl = util.$("public_list"); if (pl && thread) { - pl.prepend(`<li><underbbs-thread-summary data-len="${thread.messageCount}" data-adapter="${thread.root.data.adapter}" data-msg="${thread.root.data.id}" data-latest="${thread.latest}" data-created="${thread.created}" data-new="true"></underbbs-thread-summary></li>`); + 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) + return + } + pl.append(li); } } }

@@ -157,7 +195,11 @@ populateProfileView() {

} buildThreads() { - const datastore = _("datastore")[this._name]; + 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{

@@ -166,14 +208,23 @@ this.placeMsg(k);

} } while (this._threads.reduce((sum: number, thread: MessageThread)=>{ return sum + thread.messageCount; - }, 0) + this._orphans.length < datastore.messages.keys().length); + }, 0) + this._orphans.length < datastore.messages.size); } placeMsg(k: string): string | null { - const msg = _("datastore")[this._name].messages.get(k); + 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 (t.findNode(t.root, msg.id)) { + if (!msg || t.findNode(t.root, msg.id)) { return null; } if (msg.replyTo) {

@@ -196,6 +247,14 @@

// 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; }

@@ -206,7 +265,7 @@ // then, try to place them both

if (this.placeMsg(orphanedParent.id)) { this._orphans.splice(this._orphans.indexOf(orphanedParent), 1); - return this.placeMsg(msg); + return this.placeMsg(k); } }
M frontend/ts/adapter.tsfrontend/ts/adapter.ts

@@ -13,6 +13,8 @@ this.profileCache = new Map<string, Author>();

} } -export interface AdapterState { - [nickname: string]: AdapterData; -}+export class AdapterState { + public data: Map<string, AdapterData> = new Map<string, AdapterData>(); + + static _instance: AdapterState = new AdapterState(); +}
M frontend/ts/index.tsfrontend/ts/index.ts

@@ -1,33 +1,33 @@

+import util from "./util" import {AdapterState, AdapterData} from "./adapter"; import {Message, Attachment, Author} from "./message" -import util from "./util" +import {Settings} from "./settings" import { TabBarElement } from "./tabbar-element" import { MessageElement } from "./message-element" import { SettingsElement } from "./settings-element" import { AdapterElement } from "./adapter-element" import { ThreadSummaryElement } from "./thread-summary-element" -var $ = util.$ -var _ = util._ - function main() { - const settings = _("settings", JSON.parse(localStorage.getItem("settings") ?? "{}")); + Settings._instance = <Settings>JSON.parse(localStorage.getItem("settings") ?? "{}"); customElements.define("underbbs-tabbar", TabBarElement); customElements.define("underbbs-message", MessageElement); customElements.define("underbbs-settings", SettingsElement); customElements.define("underbbs-adapter", AdapterElement); customElements.define("underbbs-thread-summary", ThreadSummaryElement); + + util._("closeErr", util.closeErr); - tabbarInit(settings.adapters?.map((a:any)=>a.nickname) ?? []); + tabbarInit(Settings._instance.adapters?.map(a=>a.nickname) ?? []); registerServiceWorker(); } function tabbarInit(adapters: string[]) { - const nav = $("tabbar_injectparent"); + const nav = util.$("tabbar_injectparent"); if (nav) { - nav.innerHTML = `<underbbs-tabbar data-adapters="${adapters.join(",")}" data-currentadapter=""></underbbs-tabbar>`; + nav.innerHTML = `<underbbs-tabbar data-adapters="" data-currentadapter=""></underbbs-tabbar>`; } }
M frontend/ts/message.tsfrontend/ts/message.ts

@@ -9,8 +9,8 @@ public attachments: Attachment[] = [];

public replyTo: string | null = null; public replies: string[] = []; public mentions: string[] = []; - public created: Date = new Date(); - public edited: Date | null = null; + public created: number = 0; + public edited: number | null = null; public visibility: string = "public"; }
M frontend/ts/settings-element.tsfrontend/ts/settings-element.ts

@@ -1,8 +1,7 @@

import util from "./util" -import websocket from "./websocket" +import {DatagramSocket} from "./websocket" +import {Settings} from "./settings" -var $ = util.$ -var _ = util._ export class SettingsElement extends HTMLElement { static observedAttributes = [ "data-adapters" ]

@@ -18,7 +17,12 @@ this.attributeChangedCallback();

} attributeChangedCallback() { - this._adapters = this.getAttribute("data-adapters")?.split(",") ?? []; + const adapters = this.getAttribute("data-adapters"); + if (adapters) { + this._adapters = this.getAttribute("data-adapters")?.split(",") ?? []; + } else { + this._adapters = []; + } this.showSettings(this)(); }

@@ -34,19 +38,19 @@ html += "</ul>";

html += "<button id='settings_connect_btn'>connect</button>"; self.innerHTML = html; - let create = $("settings_adapter_create_btn"); + let create = util.$("settings_adapter_create_btn"); if (create) { create.addEventListener("click", self.showCreateAdapter(self), false); } for (let a of this._adapters) { - let edit = $(`settings_adapter_edit_${a}`); + let edit = util.$(`settings_adapter_edit_${a}`); if (edit) { edit.addEventListener("click", self.showEditAdapterFunc(a, self), false); } } - let connect = $("settings_connect_btn"); + let connect = util.$("settings_connect_btn"); if (connect) { - connect.addEventListener("click", websocket.connect, false); + connect.addEventListener("click", DatagramSocket.connect, false); } } }

@@ -73,17 +77,17 @@ html += "<button id='settings_adapter_create_back_btn'>back</button>";

self.innerHTML = html; - let protocolSelect = $("settings_newadapter_protocolselect"); + let protocolSelect = util.$("settings_newadapter_protocolselect"); if (protocolSelect) { protocolSelect.addEventListener("change", self.fillAdapterProtocolOptions, false); } - let save = $("settings_adapter_create_save_btn"); + let save = util.$("settings_adapter_create_save_btn"); if (save) { save.addEventListener("click", self.saveAdapter(self), false); } - let back = $("settings_adapter_create_back_btn"); + let back = util.$("settings_adapter_create_back_btn"); if (back) { back.addEventListener("click", self.showSettings(self), false); }

@@ -94,25 +98,25 @@ saveAdapter(self: SettingsElement): ()=>void {

return ()=>{ let adapterdata: any = {}; // get selected adapter protocol - const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; + const proto = util.$("settings_newadapter_protocolselect") as HTMLSelectElement; - const nickname = ($("settings_newadapter_nickname") as HTMLInputElement)?.value ?? "" ; + const nickname = (util.$("settings_newadapter_nickname") as HTMLInputElement)?.value ?? "" ; // switch protocol switch (proto.options[proto.selectedIndex].value) { case "nostr": - const privkey = ($("settings_newadapter_nostr_privkey") as HTMLInputElement)?.value ?? ""; - const relays = ($("settings_newadapter_nostr_default_relays") as HTMLInputElement)?.value ?? ""; + const privkey = (util.$("settings_newadapter_nostr_privkey") as HTMLInputElement)?.value ?? ""; + const relays = (util.$("settings_newadapter_nostr_default_relays") as HTMLInputElement)?.value ?? ""; adapterdata = { nickname: nickname, protocol: "nostr", privkey: privkey, relays: relays.split(",").map(r=>r.trim()) }; break; case "mastodon": case "misskey": - const server = ($("settings_newadapter_masto_server") as HTMLInputElement)?.value ?? ""; - const apiKey = ($("settings_newadapter_masto_apikey") as HTMLInputElement)?.value ?? ""; + const server = (util.$("settings_newadapter_masto_server") as HTMLInputElement)?.value ?? ""; + const apiKey = (util.$("settings_newadapter_masto_apikey") as HTMLInputElement)?.value ?? ""; adapterdata = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, server: server, apiKey: apiKey }; break; } - const settings = _("settings"); + const settings = Settings._instance; if (settings) { if (!settings.adapters) { settings.adapters = [];

@@ -121,13 +125,13 @@ settings.adapters.push(adapterdata);

self._adapters.push(adapterdata.nickname); localStorage.setItem("settings", JSON.stringify(settings)); - self.setAttribute("adapters", self._adapters.join(",")); + self.showSettings(self); } } } fillAdapterProtocolOptions() { - const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; + const proto = util.$("settings_newadapter_protocolselect") as HTMLSelectElement; let html = "";

@@ -145,7 +149,7 @@ html += " <label>API key<input id='settings_newadapter_masto_apikey'/></label>";

break; } - const div = $("settings_newadapter_protocoloptions"); + const div = util.$("settings_newadapter_protocoloptions"); if (div) { div.innerHTML = html; }
A frontend/ts/settings.ts

@@ -0,0 +1,19 @@

+export class AdapterConfig { + // common + public nickname: string = ""; + public protocol: string = ""; + + // masto/misskey + public server: string | null = null; + public apiKey: string | null = null; + + // nostr + public privkey: string | null = null; + public relays: string[] | null = null; +} + +export class Settings { + public adapters: AdapterConfig[] = []; + + static _instance: Settings = new Settings(); +}
M frontend/ts/tabbar-element.tsfrontend/ts/tabbar-element.ts

@@ -1,6 +1,5 @@

import util from "./util" -var _ = util._ -var $ = util.$ +import {Settings} from "./settings" export class TabBarElement extends HTMLElement { static observedAttributes = [ "data-adapters", "data-currentadapter" ]

@@ -24,7 +23,7 @@ }

attributeChangedCallback() { let html = "<ul><li><a id='tabbar_settings' href='#settings'>settings</a></li>"; - if (this.getAttribute("data-adapters") == "") { + if (!this.getAttribute("data-adapters")) { this._adapters = []; } else { this._adapters = this.getAttribute("data-adapters")?.split(",") ?? [];

@@ -43,7 +42,7 @@ html += "</ul>";

this.innerHTML = html; // now we can query the child elements and add click handlers to them - var s = $("tabbar_settings"); + var s = util.$("tabbar_settings"); if (s) { s.addEventListener("click", this.showSettings(this), false); if (!this._currentAdapter) {

@@ -51,7 +50,7 @@ s.classList.add("tabbar_current");

} } for (let i of this._adapters) { - var a = $(`tabbar_${i}`); + var a = util.$(`tabbar_${i}`); if (a) { a.addEventListener("click", this.showAdapterFunc(this, i), false); if (this._currentAdapter == i) {

@@ -64,9 +63,9 @@ }

showSettings(self: TabBarElement): ()=>void { return () => { - let x = $("mainarea_injectparent"); + let x = util.$("mainarea_injectparent"); if (x) { - x.innerHTML = `<underbbs-settings data-adapters=${self._adapters?.join(",") ?? []}></underbbs-settings>`; + x.innerHTML = `<underbbs-settings data-adapters=${Settings._instance.adapters.map(a=>a.nickname).join(",") ?? []}></underbbs-settings>`; self.setAttribute("data-currentadapter", ""); } }

@@ -74,7 +73,7 @@ }

showAdapterFunc(self: TabBarElement, adapter: string): ()=>void { return ()=>{ - let x = $("mainarea_injectparent"); + let x = util.$("mainarea_injectparent"); if (x) { x.innerHTML = `<underbbs-adapter id="adapter_${adapter}" data-name="${adapter}"></underbbs-adapter>`; self.setAttribute("data-currentadapter", adapter);
M frontend/ts/thread-summary-element.tsfrontend/ts/thread-summary-element.ts

@@ -1,7 +1,6 @@

import util from "./util" import { Message, Author } from "./message" -var _ = util._ -var $ = util.$ +import { AdapterState } from "./adapter" export class ThreadSummaryElement extends HTMLElement { static observedAttributes = [ "data-len", "data-author", "data-latest", "data-new" ];

@@ -10,8 +9,8 @@ private _len: number = 0;;

private _msg: Message | null = null;; private _author: Author | null = null; private _adapter: string = ""; - private _created: Date = new Date(); - private _latest: Date = new Date(); + private _created: number = 0; + private _latest: number = 0; private _new: boolean = false; constructor() {

@@ -30,10 +29,10 @@ }

} attributeChangedCallback() { - const datastore = _("datastore")[this._adapter]; + const datastore = AdapterState._instance.data.get(this._adapter); const msgId = this.getAttribute("data-msg"); if (msgId && datastore && !this._msg) { - this._msg = datastore.messages.get(msgId); + this._msg = datastore.messages.get(msgId) || null; if (this._msg) { const threadText = this.querySelector(".thread_text"); if (threadText) {

@@ -59,14 +58,14 @@

// update author if it's passed in the attribute const authorId = this.getAttribute("data-author"); if (authorId) { - let author = datastore?.profileCache?.get(this._msg?.author); + let author = datastore?.profileCache?.get(this._msg?.author || ""); if (author) { this._author = author; const threadAuthor = this.querySelector(".thread_author"); if (threadAuthor && this._author && this._msg) { threadAuthor.innerHTML = this._author.profilePic - ? `<img src="${this._author.profilePic}" alt="${this._author.id}"/> <a id="thread_${this._adapter}_${this._msg.id}_${this._author.id}" href="#author?id=${this._author.id}>${this._author.id}</a>` - : `<a id="thread_${this._adapter}_${this._msg.id}_${this._author.id}" href="#author?id=${author.id}>${this._author.id}</a>` ; + ? `<img src="${this._author.profilePic}" alt="${this._author.id}"/> <a id="thread_${this._adapter}_${this._msg.id}_${this._author.id}" href="#author?id=${this._author.id}">${this._author.id}</a>` + : `<a id="thread_${this._adapter}_${this._msg.id}_${this._author.id}" href="#author?id=${author.id}">${this._author.id}</a>` ; } } }

@@ -82,14 +81,14 @@ if (l && l != this._len) {

metadataChanged = true; this._len = l; } - if (created && new Date(created) != this._created) { + if (created && parseInt(created) != this._created) { metadataChanged = true; - this._created = new Date(created); + this._created = parseInt(created); this._latest = this._created; } - if (latest && new Date(latest) != this._latest) { + if (latest && parseInt(latest) != this._latest) { metadataChanged = true; - this._latest = new Date(latest); + this._latest = parseInt(latest); } if (newness != this._new) {

@@ -106,7 +105,7 @@ }

} viewThread(self: ThreadSummaryElement) { return () => { - const a = $(`adapter_${self._adapter}`); + const a = util.$(`adapter_${self._adapter}`); if (a && self._msg) { a.setAttribute("data-view", "thread"); a.setAttribute("data-viewing", self._msg.id);
M frontend/ts/thread.tsfrontend/ts/thread.ts

@@ -1,7 +1,5 @@

import util from "./util" import { Message } from "./message" -var _ = util._ -var $ = util.$ export class MessageNode { public parent: MessageNode | null = null;

@@ -27,8 +25,8 @@ export class MessageThread {

public root: MessageNode; public messageCount: number; public visibility: string; - public created: Date; - public latest: Date; + public created: number; + public latest: number; constructor(first: Message) { this.root = new MessageNode(first);

@@ -44,7 +42,7 @@ if (node) {

node.children.push(new MessageNode(reply, node)); this.messageCount++; const mtime = reply.edited ? reply.edited : reply.created; - if (this.latest.getTime() < mtime.getTime()) { + if (this.latest < mtime) { this.latest = mtime; } return true;

@@ -58,7 +56,7 @@ return node;

} else { for (let n of node.children) { const x = this.findNode(n, id); - if (x != null) { + if (x) { return x; } }
M frontend/ts/util.tsfrontend/ts/util.ts

@@ -1,3 +1,4 @@

+import { DatagramSocket } from './websocket' function _(key: string, value: any | null | undefined = undefined): any | null { const x = <any>window;

@@ -11,9 +12,25 @@ function $(id: string): HTMLElement | null {

return document.getElementById(id); } +function errMsg(msg: string): void { + const div = $("err_div"); + const w = $("err_wrapper"); + if (div && w) { + div.innerText = msg; + w.style.display = "block"; + } +} + +function closeErr(): void { + const w = $("err_wrapper"); + if (w) { + w.style.display = "none"; + } +} + async function authorizedFetch(method: string, uri: string, body: any): Promise<Response> { const headers = new Headers() - headers.set('Authorization', 'Bearer ' + _("skey")) + headers.set('Authorization', 'Bearer ' + DatagramSocket.skey) return await fetch(uri, { method: method, headers: headers,

@@ -21,4 +38,4 @@ body: body,

}) } -export default { _, $, authorizedFetch }+export default { _, $, authorizedFetch, errMsg, closeErr }
M frontend/ts/websocket.tsfrontend/ts/websocket.ts

@@ -1,54 +1,75 @@

import util from "./util" import {AdapterState, AdapterData} from "./adapter"; import {Message, Attachment, Author} from "./message" +import {Settings} from "./settings" -var $ = util.$ -var _ = util._ -function connect() { +export class DatagramSocket { + public static skey: string | null = null; + public static conn: WebSocket | null; - let datastore = <AdapterState>_("datastore", {}); - const wsProto = location.protocol == "https:" ? "wss" : "ws"; - const _conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, "underbbs"); - _conn.addEventListener("open", (e: any) => { - console.log("websocket connection opened"); - console.log(JSON.stringify(e)); - }); - _conn.addEventListener("message", (e: any) => { - const data = JSON.parse(e.data); - console.log(data); - if (data.key) { - _("skey", data.key) - util.authorizedFetch("POST", "/api/adapters", JSON.stringify(_("settings").adapters)) - } else { - if (!datastore[data.adapter]) { - datastore[data.adapter] = new AdapterData(data.protocol); + private static onOpen(e: Event) { + console.log("websocket connection opened"); + console.log(JSON.stringify(e)); + } + + private static onMsg(e: MessageEvent) { + const data = JSON.parse(e.data); + console.log(data); + if (data.key) { + DatagramSocket.skey = data.key; + util.authorizedFetch("POST", "/api/adapters", JSON.stringify(Settings._instance.adapters)) + .then(r=> { + if (r.ok) { + const tabbar = document.querySelector("underbbs-tabbar"); + if (tabbar) { + tabbar.setAttribute("data-adapters", Settings._instance.adapters.map(a=>a.nickname).join(",")); + } } - + }) + .catch(e => { + util.errMsg(e.message); + }); + } else { + let store = AdapterState._instance.data.get(data.adapter); + if (!store) { + AdapterState._instance.data.set(data.adapter, new AdapterData(data.protocol)); + store = AdapterState._instance.data.get(data.adapter); + } else { // typeswitch on the incoming data type and fill the memory switch (data.type) { case "message": - datastore[data.adapter].messages.set(data.id, <Message>data); + store.messages.set(data.id, <Message>data); break; case "author": - datastore[data.adapter].profileCache.set(data.id, <Author>data); + store.profileCache.set(data.id, <Author>data); break; default: break; } - // if the adapter is active signal it that there's new data - let adapter = $(`adapter_${data.adapter}`); - if (adapter) { - adapter.setAttribute("data-latest", data.id); - } } - }); + // if the adapter is active signal it that there's new data + let adapter = util.$(`adapter_${data.adapter}`); + if (adapter) { + adapter.setAttribute("data-latest", data.id); + } + } + } + + static connect(): void { + + + + const wsProto = location.protocol == "https:" ? "wss" : "ws"; + const _conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, "underbbs"); + _conn.addEventListener("open", DatagramSocket.onOpen); + _conn.addEventListener("message", DatagramSocket.onMsg); _conn.addEventListener("error", (e: any) => { console.log("websocket connection error"); console.log(JSON.stringify(e)); }); - _("websocket", _conn); + DatagramSocket.conn = _conn; + } } -export default { connect }
M models/msg.gomodels/msg.go

@@ -2,7 +2,6 @@ package models

import ( "encoding/json" - "time" ) type Datagram struct {

@@ -12,6 +11,8 @@ Protocol string `json:"protocol"`

Adapter string `json:"adapter"` Type string `json:"type"` Target *string `json:"target,omitempty"` + Created int64 `json:"created"` + Updated *int64 `json:"updated,omitempty"` } type Message struct {

@@ -23,8 +24,6 @@ ReplyTo *string `json:"replyTo"`

Replies []string `json:"replies"` ReplyCount int `json:"replyCount"` Mentions []string `json:"mentions"` - Created time.Time `json:"created"` - Edited *time.Time `json:"edited,omitempty"` Visibility string `json:"visibility"` }

@@ -40,7 +39,7 @@ type Attachment struct {

Src string `json:"src"` ThumbSrc string `json:"thumbSrc"` Desc string `json:"desc"` - CreatedAt time.Time `json:"createdAt"` + Created int64 `json:"created"` Size uint64 `json:"size"` }