use static classes for Settings and AdapterState

This commit is contained in:
Iris Lightshard 2024-07-16 13:43:35 -06:00
parent fead16168a
commit 2976d438d9
Signed by: Iris Lightshard
GPG key ID: 688407174966CAF3
17 changed files with 262 additions and 140 deletions

View file

@ -10,11 +10,17 @@ each distinct `adapter` connection/configuration is represented in the frontend
adapters receive commands via a quartzgun web API and send data back on their shared websocket connection adapters receive commands via a quartzgun web API and send data back on their shared websocket connection
## building ## building and running
requirements are requirements are
- go 1.22 - go 1.22
- any recent nodejs that can do `typescript` and `webpack` 5 - 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`

View file

@ -110,10 +110,10 @@ func (self *MastoAdapter) mastoUpdateToMessage(status madon.Status) *Message {
Id: fmt.Sprintf("%d", status.ID), Id: fmt.Sprintf("%d", status.ID),
Uri: status.URI, Uri: status.URI,
Type: "message", Type: "message",
Created: status.CreatedAt.UnixMilli(),
}, },
Content: status.Content, Content: status.Content,
Author: status.Account.Acct, Author: status.Account.Acct,
Created: status.CreatedAt,
Visibility: status.Visibility, Visibility: status.Visibility,
} }
if status.InReplyToID != nil { if status.InReplyToID != nil {

View file

@ -205,10 +205,9 @@ func (self *MisskeyAdapter) toMessage(n mkm.Note, bustCache bool) *Message {
Protocol: "misskey", Protocol: "misskey",
Adapter: self.nickname, Adapter: self.nickname,
Type: "message", Type: "message",
Created: n.CreatedAt.UnixMilli(),
}, },
Created: n.CreatedAt,
Author: authorId, Author: authorId,
Content: n.Text, Content: n.Text,
Attachments: []Attachment{}, Attachments: []Attachment{},
@ -224,7 +223,7 @@ func (self *MisskeyAdapter) toMessage(n mkm.Note, bustCache bool) *Message {
ThumbSrc: f.ThumbnailURL, ThumbSrc: f.ThumbnailURL,
Size: f.Size, Size: f.Size,
Desc: f.Comment, Desc: f.Comment,
CreatedAt: f.CreatedAt, Created: f.CreatedAt.UnixMilli(),
}) })
} }
return &msg return &msg
@ -242,6 +241,12 @@ func (self *MisskeyAdapter) toAuthor(usr mkm.User) *Author {
authorId = fmt.Sprintf("@%s", usr.Username) authorId = fmt.Sprintf("@%s", usr.Username)
} }
var updated *int64 = nil
if usr.UpdatedAt != nil {
updatedTmp := usr.UpdatedAt.UnixMilli()
updated = &updatedTmp
}
author := Author{ author := Author{
Datagram: Datagram{ Datagram: Datagram{
Id: authorId, Id: authorId,
@ -249,6 +254,8 @@ func (self *MisskeyAdapter) toAuthor(usr mkm.User) *Author {
Protocol: "misskey", Protocol: "misskey",
Adapter: self.nickname, Adapter: self.nickname,
Type: "author", Type: "author",
Created: usr.CreatedAt.UnixMilli(),
Updated: updated,
}, },
Name: usr.Name, Name: usr.Name,
ProfilePic: usr.AvatarURL, ProfilePic: usr.AvatarURL,
@ -303,7 +310,6 @@ func (self *MisskeyAdapter) Fetch(etype, id string) error {
case "author": case "author":
user := "" user := ""
host := "" host := ""
fmt.Printf("fetch author: %s\n", id)
idParts := strings.Split(id, "@") idParts := strings.Split(id, "@")
user = idParts[1] user = idParts[1]
if len(idParts) == 3 { if len(idParts) == 3 {
@ -315,12 +321,6 @@ func (self *MisskeyAdapter) Fetch(etype, id string) error {
hostPtr = &host 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) // fmt.Printf("attempting user resolution: @%s@%s\n", user, host)
data, err := self.mk.Users().Show(users.ShowRequest{ data, err := self.mk.Users().Show(users.ShowRequest{
Username: &user, Username: &user,

View file

@ -106,7 +106,7 @@ func (self *NostrAdapter) nostrEventToMsg(evt *nostr.Event) (Message, error) {
case nostr.KindTextNote: case nostr.KindTextNote:
m.Id = evt.ID m.Id = evt.ID
m.Author = evt.PubKey m.Author = evt.PubKey
m.Created = evt.CreatedAt.Time() m.Created = evt.CreatedAt.Time().UnixMilli()
m.Content = evt.Content m.Content = evt.Content
return m, nil return m, nil
default: default:

View file

@ -23,7 +23,6 @@ case "$1" in
go build go build
;; ;;
*) *)
$0 client echo "usage: ${0} <front|server>"
$0 server
;; ;;
esac esac

View file

@ -1,9 +1,8 @@
import util from "./util" import util from "./util"
import { Message, Author } from "./message" import { Message, Author } from "./message"
import { MessageThread } from "./thread" import { MessageThread } from "./thread"
import { AdapterState } from "./adapter"
var _ = util._
var $ = util.$
export class AdapterElement extends HTMLElement { export class AdapterElement extends HTMLElement {
static observedAttributes = [ "data-latest", "data-view", "data-viewing" ] static observedAttributes = [ "data-latest", "data-view", "data-viewing" ]
@ -30,14 +29,17 @@ export class AdapterElement extends HTMLElement {
} }
attributeChangedCallback() { attributeChangedCallback() {
console.log(`${this._name}.attributeChangedCallback: start`);
// set the viewing subject if it's changed // set the viewing subject if it's changed
const viewing = this.getAttribute("data-viewing"); const viewing = this.getAttribute("data-viewing");
if (this._viewing != viewing && viewing != null) { if (this._viewing != viewing && viewing != null) {
console.log(`${this._name}.attributeChangedCallback: resetting viewing subject`);
this._viewing = viewing; this._viewing = viewing;
// if the viewing subject changed (not to nothing), unset the view // if the viewing subject changed (not to nothing), unset the view
// this will force it to refresh // this will force it to refresh
if (this._viewing) { if (this._viewing) {
console.log(`${this._name}.attributeChangedCallback: forcing view update`);
this._view = ""; this._view = "";
} }
} }
@ -46,6 +48,8 @@ export class AdapterElement extends HTMLElement {
const view = this.getAttribute("data-view"); const view = this.getAttribute("data-view");
if (this._view != view ?? "index") { if (this._view != view ?? "index") {
this._view = view ?? "index"; this._view = view ?? "index";
console.log(`${this._name}.attributeChangedCallback: setting view: ${this._view}`);
switch (this._view) { switch (this._view) {
case "index": case "index":
this.setIdxView(); this.setIdxView();
@ -63,18 +67,25 @@ export class AdapterElement extends HTMLElement {
} }
// if latest changed, check if it's a message // 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) { if (latest ?? "" != this._latest) {
console.log("latest changed")
this._latest = latest ?? ""; this._latest = latest ?? "";
let datastore = _("datastore"); let datastore = AdapterState._instance.data.get(this._name);
console.log(datastore); if (!datastore) {
util.errMsg(this._name + " has no datastore!");
return;
}
const latestMsg = datastore.messages.get(this._latest); const latestMsg = datastore.messages.get(this._latest);
if (latestMsg) { if (latestMsg) {
console.log('latest was a message; place it');
const rootId = this.placeMsg(this._latest); 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 is null, this is an orphan and we don't need to actually do any updates yet
if (rootId) { if (rootId) {
switch (this._view) { switch (this._view) {
case "index": case "index":
console.log(`message was placed in thread ${rootId}, update view`)
this.updateIdxView(this._latest, rootId); this.updateIdxView(this._latest, rootId);
break; break;
case "thread": case "thread":
@ -85,12 +96,23 @@ export class AdapterElement extends HTMLElement {
} }
} }
} else { } else {
const latestAuthor = _("datastore")[this._name].profileCache.get(this._latest); const latestAuthor = datastore.profileCache.get(this._latest);
switch (this._view) { if (latestAuthor) {
case "index": switch (this._view) {
case "thread": case "index":
case "profile": console.log (`author was updated: ${this._latest}, update their threads`)
break; 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 // so, try to insert it into the threads
@ -113,7 +135,7 @@ export class AdapterElement extends HTMLElement {
} }
setProfileView() { setProfileView() {
let profile_bar = $("profile_bar"); let profile_bar = util.$("profile_bar");
if (profile_bar) { if (profile_bar) {
// clear any previous data // clear any previous data
} else { } else {
@ -124,28 +146,44 @@ export class AdapterElement extends HTMLElement {
populateIdxView() { populateIdxView() {
// skip dm list for now // skip dm list for now
// public/unified list // public/unified list
const pl = $("public_list"); const pl = util.$("public_list");
if (pl) { if (pl) {
let html = ""; let html = "";
for (const t of this._threads) { 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}"></underbbs-thread-summary></li>`; 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; pl.innerHTML = html;
} }
} }
updateIdxView(latest: string, rootId: string) { 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); const thread = this._threads.find(t=>t.root.data.id == rootId);
if (existingThread && thread) { 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-len", `${thread.messageCount}`);
existingThread.setAttribute("data-new", "true"); existingThread.setAttribute("data-new", "true");
} else { } else {
// unified/public list for now // unified/public list for now
const pl = $("public_list"); const pl = util.$("public_list");
if (pl && thread) { 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 @@ export class AdapterElement extends HTMLElement {
} }
buildThreads() { 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 // 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 // placed in a thread, or orphaned and waiting for its parent to be returned
do{ do{
@ -166,14 +208,23 @@ export class AdapterElement extends HTMLElement {
} }
} while (this._threads.reduce((sum: number, thread: MessageThread)=>{ } while (this._threads.reduce((sum: number, thread: MessageThread)=>{
return sum + thread.messageCount; return sum + thread.messageCount;
}, 0) + this._orphans.length < datastore.messages.keys().length); }, 0) + this._orphans.length < datastore.messages.size);
} }
placeMsg(k: string): string | null { 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) { for (let t of this._threads) {
// avoid processing nodes again on subsequent passes // avoid processing nodes again on subsequent passes
if (t.findNode(t.root, msg.id)) { if (!msg || t.findNode(t.root, msg.id)) {
return null; return null;
} }
if (msg.replyTo) { if (msg.replyTo) {
@ -196,6 +247,14 @@ export class AdapterElement extends HTMLElement {
// if it doesn't have a parent, we can make a new thread with it // if it doesn't have a parent, we can make a new thread with it
if (!msg.replyTo) { if (!msg.replyTo) {
this._threads.push(new MessageThread(msg)); 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; return msg.id;
} }
@ -206,7 +265,7 @@ export class AdapterElement extends HTMLElement {
if (this.placeMsg(orphanedParent.id)) { if (this.placeMsg(orphanedParent.id)) {
this._orphans.splice(this._orphans.indexOf(orphanedParent), 1); this._orphans.splice(this._orphans.indexOf(orphanedParent), 1);
return this.placeMsg(msg); return this.placeMsg(k);
} }
} }

View file

@ -13,6 +13,8 @@ export class AdapterData {
} }
} }
export interface AdapterState { export class AdapterState {
[nickname: string]: AdapterData; public data: Map<string, AdapterData> = new Map<string, AdapterData>();
}
static _instance: AdapterState = new AdapterState();
}

View file

@ -1,33 +1,33 @@
import util from "./util"
import {AdapterState, AdapterData} from "./adapter"; import {AdapterState, AdapterData} from "./adapter";
import {Message, Attachment, Author} from "./message" import {Message, Attachment, Author} from "./message"
import util from "./util" import {Settings} from "./settings"
import { TabBarElement } from "./tabbar-element" import { TabBarElement } from "./tabbar-element"
import { MessageElement } from "./message-element" import { MessageElement } from "./message-element"
import { SettingsElement } from "./settings-element" import { SettingsElement } from "./settings-element"
import { AdapterElement } from "./adapter-element" import { AdapterElement } from "./adapter-element"
import { ThreadSummaryElement } from "./thread-summary-element" import { ThreadSummaryElement } from "./thread-summary-element"
var $ = util.$
var _ = util._
function main() { 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-tabbar", TabBarElement);
customElements.define("underbbs-message", MessageElement); customElements.define("underbbs-message", MessageElement);
customElements.define("underbbs-settings", SettingsElement); customElements.define("underbbs-settings", SettingsElement);
customElements.define("underbbs-adapter", AdapterElement); customElements.define("underbbs-adapter", AdapterElement);
customElements.define("underbbs-thread-summary", ThreadSummaryElement); 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(); registerServiceWorker();
} }
function tabbarInit(adapters: string[]) { function tabbarInit(adapters: string[]) {
const nav = $("tabbar_injectparent"); const nav = util.$("tabbar_injectparent");
if (nav) { if (nav) {
nav.innerHTML = `<underbbs-tabbar data-adapters="${adapters.join(",")}" data-currentadapter=""></underbbs-tabbar>`; nav.innerHTML = `<underbbs-tabbar data-adapters="" data-currentadapter=""></underbbs-tabbar>`;
} }
} }

View file

@ -9,8 +9,8 @@ export class Message {
public replyTo: string | null = null; public replyTo: string | null = null;
public replies: string[] = []; public replies: string[] = [];
public mentions: string[] = []; public mentions: string[] = [];
public created: Date = new Date(); public created: number = 0;
public edited: Date | null = null; public edited: number | null = null;
public visibility: string = "public"; public visibility: string = "public";
} }

View file

@ -1,8 +1,7 @@
import util from "./util" 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 { export class SettingsElement extends HTMLElement {
static observedAttributes = [ "data-adapters" ] static observedAttributes = [ "data-adapters" ]
@ -18,7 +17,12 @@ export class SettingsElement extends HTMLElement {
} }
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)(); this.showSettings(this)();
} }
@ -34,19 +38,19 @@ export class SettingsElement extends HTMLElement {
html += "<button id='settings_connect_btn'>connect</button>"; html += "<button id='settings_connect_btn'>connect</button>";
self.innerHTML = html; self.innerHTML = html;
let create = $("settings_adapter_create_btn"); let create = util.$("settings_adapter_create_btn");
if (create) { if (create) {
create.addEventListener("click", self.showCreateAdapter(self), false); create.addEventListener("click", self.showCreateAdapter(self), false);
} }
for (let a of this._adapters) { for (let a of this._adapters) {
let edit = $(`settings_adapter_edit_${a}`); let edit = util.$(`settings_adapter_edit_${a}`);
if (edit) { if (edit) {
edit.addEventListener("click", self.showEditAdapterFunc(a, self), false); edit.addEventListener("click", self.showEditAdapterFunc(a, self), false);
} }
} }
let connect = $("settings_connect_btn"); let connect = util.$("settings_connect_btn");
if (connect) { if (connect) {
connect.addEventListener("click", websocket.connect, false); connect.addEventListener("click", DatagramSocket.connect, false);
} }
} }
} }
@ -73,17 +77,17 @@ export class SettingsElement extends HTMLElement {
self.innerHTML = html; self.innerHTML = html;
let protocolSelect = $("settings_newadapter_protocolselect"); let protocolSelect = util.$("settings_newadapter_protocolselect");
if (protocolSelect) { if (protocolSelect) {
protocolSelect.addEventListener("change", self.fillAdapterProtocolOptions, false); protocolSelect.addEventListener("change", self.fillAdapterProtocolOptions, false);
} }
let save = $("settings_adapter_create_save_btn"); let save = util.$("settings_adapter_create_save_btn");
if (save) { if (save) {
save.addEventListener("click", self.saveAdapter(self), false); save.addEventListener("click", self.saveAdapter(self), false);
} }
let back = $("settings_adapter_create_back_btn"); let back = util.$("settings_adapter_create_back_btn");
if (back) { if (back) {
back.addEventListener("click", self.showSettings(self), false); back.addEventListener("click", self.showSettings(self), false);
} }
@ -94,25 +98,25 @@ export class SettingsElement extends HTMLElement {
return ()=>{ return ()=>{
let adapterdata: any = {}; let adapterdata: any = {};
// get selected adapter protocol // 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 protocol
switch (proto.options[proto.selectedIndex].value) { switch (proto.options[proto.selectedIndex].value) {
case "nostr": case "nostr":
const privkey = ($("settings_newadapter_nostr_privkey") as HTMLInputElement)?.value ?? ""; const privkey = (util.$("settings_newadapter_nostr_privkey") as HTMLInputElement)?.value ?? "";
const relays = ($("settings_newadapter_nostr_default_relays") 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()) }; adapterdata = { nickname: nickname, protocol: "nostr", privkey: privkey, relays: relays.split(",").map(r=>r.trim()) };
break; break;
case "mastodon": case "mastodon":
case "misskey": case "misskey":
const server = ($("settings_newadapter_masto_server") as HTMLInputElement)?.value ?? ""; const server = (util.$("settings_newadapter_masto_server") as HTMLInputElement)?.value ?? "";
const apiKey = ($("settings_newadapter_masto_apikey") 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 }; adapterdata = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, server: server, apiKey: apiKey };
break; break;
} }
const settings = _("settings"); const settings = Settings._instance;
if (settings) { if (settings) {
if (!settings.adapters) { if (!settings.adapters) {
settings.adapters = []; settings.adapters = [];
@ -121,13 +125,13 @@ export class SettingsElement extends HTMLElement {
self._adapters.push(adapterdata.nickname); self._adapters.push(adapterdata.nickname);
localStorage.setItem("settings", JSON.stringify(settings)); localStorage.setItem("settings", JSON.stringify(settings));
self.setAttribute("adapters", self._adapters.join(",")); self.showSettings(self);
} }
} }
} }
fillAdapterProtocolOptions() { fillAdapterProtocolOptions() {
const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; const proto = util.$("settings_newadapter_protocolselect") as HTMLSelectElement;
let html = ""; let html = "";
@ -145,7 +149,7 @@ export class SettingsElement extends HTMLElement {
break; break;
} }
const div = $("settings_newadapter_protocoloptions"); const div = util.$("settings_newadapter_protocoloptions");
if (div) { if (div) {
div.innerHTML = html; div.innerHTML = html;
} }

19
frontend/ts/settings.ts Normal file
View file

@ -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();
}

View file

@ -1,6 +1,5 @@
import util from "./util" import util from "./util"
var _ = util._ import {Settings} from "./settings"
var $ = util.$
export class TabBarElement extends HTMLElement { export class TabBarElement extends HTMLElement {
static observedAttributes = [ "data-adapters", "data-currentadapter" ] static observedAttributes = [ "data-adapters", "data-currentadapter" ]
@ -24,7 +23,7 @@ export class TabBarElement extends HTMLElement {
attributeChangedCallback() { attributeChangedCallback() {
let html = "<ul><li><a id='tabbar_settings' href='#settings'>settings</a></li>"; 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 = []; this._adapters = [];
} else { } else {
this._adapters = this.getAttribute("data-adapters")?.split(",") ?? []; this._adapters = this.getAttribute("data-adapters")?.split(",") ?? [];
@ -43,7 +42,7 @@ export class TabBarElement extends HTMLElement {
this.innerHTML = html; this.innerHTML = html;
// now we can query the child elements and add click handlers to them // now we can query the child elements and add click handlers to them
var s = $("tabbar_settings"); var s = util.$("tabbar_settings");
if (s) { if (s) {
s.addEventListener("click", this.showSettings(this), false); s.addEventListener("click", this.showSettings(this), false);
if (!this._currentAdapter) { if (!this._currentAdapter) {
@ -51,7 +50,7 @@ export class TabBarElement extends HTMLElement {
} }
} }
for (let i of this._adapters) { for (let i of this._adapters) {
var a = $(`tabbar_${i}`); var a = util.$(`tabbar_${i}`);
if (a) { if (a) {
a.addEventListener("click", this.showAdapterFunc(this, i), false); a.addEventListener("click", this.showAdapterFunc(this, i), false);
if (this._currentAdapter == i) { if (this._currentAdapter == i) {
@ -64,9 +63,9 @@ export class TabBarElement extends HTMLElement {
showSettings(self: TabBarElement): ()=>void { showSettings(self: TabBarElement): ()=>void {
return () => { return () => {
let x = $("mainarea_injectparent"); let x = util.$("mainarea_injectparent");
if (x) { 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", ""); self.setAttribute("data-currentadapter", "");
} }
} }
@ -74,7 +73,7 @@ export class TabBarElement extends HTMLElement {
showAdapterFunc(self: TabBarElement, adapter: string): ()=>void { showAdapterFunc(self: TabBarElement, adapter: string): ()=>void {
return ()=>{ return ()=>{
let x = $("mainarea_injectparent"); let x = util.$("mainarea_injectparent");
if (x) { if (x) {
x.innerHTML = `<underbbs-adapter id="adapter_${adapter}" data-name="${adapter}"></underbbs-adapter>`; x.innerHTML = `<underbbs-adapter id="adapter_${adapter}" data-name="${adapter}"></underbbs-adapter>`;
self.setAttribute("data-currentadapter", adapter); self.setAttribute("data-currentadapter", adapter);

View file

@ -1,7 +1,6 @@
import util from "./util" import util from "./util"
import { Message, Author } from "./message" import { Message, Author } from "./message"
var _ = util._ import { AdapterState } from "./adapter"
var $ = util.$
export class ThreadSummaryElement extends HTMLElement { export class ThreadSummaryElement extends HTMLElement {
static observedAttributes = [ "data-len", "data-author", "data-latest", "data-new" ]; static observedAttributes = [ "data-len", "data-author", "data-latest", "data-new" ];
@ -10,8 +9,8 @@ export class ThreadSummaryElement extends HTMLElement {
private _msg: Message | null = null;; private _msg: Message | null = null;;
private _author: Author | null = null; private _author: Author | null = null;
private _adapter: string = ""; private _adapter: string = "";
private _created: Date = new Date(); private _created: number = 0;
private _latest: Date = new Date(); private _latest: number = 0;
private _new: boolean = false; private _new: boolean = false;
constructor() { constructor() {
@ -30,10 +29,10 @@ export class ThreadSummaryElement extends HTMLElement {
} }
attributeChangedCallback() { attributeChangedCallback() {
const datastore = _("datastore")[this._adapter]; const datastore = AdapterState._instance.data.get(this._adapter);
const msgId = this.getAttribute("data-msg"); const msgId = this.getAttribute("data-msg");
if (msgId && datastore && !this._msg) { if (msgId && datastore && !this._msg) {
this._msg = datastore.messages.get(msgId); this._msg = datastore.messages.get(msgId) || null;
if (this._msg) { if (this._msg) {
const threadText = this.querySelector(".thread_text"); const threadText = this.querySelector(".thread_text");
if (threadText) { if (threadText) {
@ -59,14 +58,14 @@ export class ThreadSummaryElement extends HTMLElement {
// update author if it's passed in the attribute // update author if it's passed in the attribute
const authorId = this.getAttribute("data-author"); const authorId = this.getAttribute("data-author");
if (authorId) { if (authorId) {
let author = datastore?.profileCache?.get(this._msg?.author); let author = datastore?.profileCache?.get(this._msg?.author || "");
if (author) { if (author) {
this._author = author; this._author = author;
const threadAuthor = this.querySelector(".thread_author"); const threadAuthor = this.querySelector(".thread_author");
if (threadAuthor && this._author && this._msg) { if (threadAuthor && this._author && this._msg) {
threadAuthor.innerHTML = this._author.profilePic 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>` ? `<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>` ; : `<a id="thread_${this._adapter}_${this._msg.id}_${this._author.id}" href="#author?id=${author.id}">${this._author.id}</a>` ;
} }
} }
} }
@ -82,14 +81,14 @@ export class ThreadSummaryElement extends HTMLElement {
metadataChanged = true; metadataChanged = true;
this._len = l; this._len = l;
} }
if (created && new Date(created) != this._created) { if (created && parseInt(created) != this._created) {
metadataChanged = true; metadataChanged = true;
this._created = new Date(created); this._created = parseInt(created);
this._latest = this._created; this._latest = this._created;
} }
if (latest && new Date(latest) != this._latest) { if (latest && parseInt(latest) != this._latest) {
metadataChanged = true; metadataChanged = true;
this._latest = new Date(latest); this._latest = parseInt(latest);
} }
if (newness != this._new) { if (newness != this._new) {
@ -106,7 +105,7 @@ export class ThreadSummaryElement extends HTMLElement {
} }
viewThread(self: ThreadSummaryElement) { viewThread(self: ThreadSummaryElement) {
return () => { return () => {
const a = $(`adapter_${self._adapter}`); const a = util.$(`adapter_${self._adapter}`);
if (a && self._msg) { if (a && self._msg) {
a.setAttribute("data-view", "thread"); a.setAttribute("data-view", "thread");
a.setAttribute("data-viewing", self._msg.id); a.setAttribute("data-viewing", self._msg.id);

View file

@ -1,7 +1,5 @@
import util from "./util" import util from "./util"
import { Message } from "./message" import { Message } from "./message"
var _ = util._
var $ = util.$
export class MessageNode { export class MessageNode {
public parent: MessageNode | null = null; public parent: MessageNode | null = null;
@ -27,8 +25,8 @@ export class MessageThread {
public root: MessageNode; public root: MessageNode;
public messageCount: number; public messageCount: number;
public visibility: string; public visibility: string;
public created: Date; public created: number;
public latest: Date; public latest: number;
constructor(first: Message) { constructor(first: Message) {
this.root = new MessageNode(first); this.root = new MessageNode(first);
@ -44,7 +42,7 @@ export class MessageThread {
node.children.push(new MessageNode(reply, node)); node.children.push(new MessageNode(reply, node));
this.messageCount++; this.messageCount++;
const mtime = reply.edited ? reply.edited : reply.created; const mtime = reply.edited ? reply.edited : reply.created;
if (this.latest.getTime() < mtime.getTime()) { if (this.latest < mtime) {
this.latest = mtime; this.latest = mtime;
} }
return true; return true;
@ -58,7 +56,7 @@ export class MessageThread {
} else { } else {
for (let n of node.children) { for (let n of node.children) {
const x = this.findNode(n, id); const x = this.findNode(n, id);
if (x != null) { if (x) {
return x; return x;
} }
} }

View file

@ -1,3 +1,4 @@
import { DatagramSocket } from './websocket'
function _(key: string, value: any | null | undefined = undefined): any | null { function _(key: string, value: any | null | undefined = undefined): any | null {
const x = <any>window; const x = <any>window;
@ -11,9 +12,25 @@ function $(id: string): HTMLElement | null {
return document.getElementById(id); 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> { async function authorizedFetch(method: string, uri: string, body: any): Promise<Response> {
const headers = new Headers() const headers = new Headers()
headers.set('Authorization', 'Bearer ' + _("skey")) headers.set('Authorization', 'Bearer ' + DatagramSocket.skey)
return await fetch(uri, { return await fetch(uri, {
method: method, method: method,
headers: headers, headers: headers,
@ -21,4 +38,4 @@ async function authorizedFetch(method: string, uri: string, body: any): Promise<
}) })
} }
export default { _, $, authorizedFetch } export default { _, $, authorizedFetch, errMsg, closeErr }

View file

@ -1,54 +1,75 @@
import util from "./util" import util from "./util"
import {AdapterState, AdapterData} from "./adapter"; import {AdapterState, AdapterData} from "./adapter";
import {Message, Attachment, Author} from "./message" 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"; private static onOpen(e: Event) {
const _conn = new WebSocket(`${wsProto}://${location.host}/subscribe`, "underbbs"); console.log("websocket connection opened");
_conn.addEventListener("open", (e: any) => { console.log(JSON.stringify(e));
console.log("websocket connection opened"); }
console.log(JSON.stringify(e));
}); private static onMsg(e: MessageEvent) {
_conn.addEventListener("message", (e: any) => { const data = JSON.parse(e.data);
const data = JSON.parse(e.data); console.log(data);
console.log(data); if (data.key) {
if (data.key) { DatagramSocket.skey = data.key;
_("skey", data.key) util.authorizedFetch("POST", "/api/adapters", JSON.stringify(Settings._instance.adapters))
util.authorizedFetch("POST", "/api/adapters", JSON.stringify(_("settings").adapters)) .then(r=> {
} else { if (r.ok) {
if (!datastore[data.adapter]) { const tabbar = document.querySelector("underbbs-tabbar");
datastore[data.adapter] = new AdapterData(data.protocol); 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 // typeswitch on the incoming data type and fill the memory
switch (data.type) { switch (data.type) {
case "message": case "message":
datastore[data.adapter].messages.set(data.id, <Message>data); store.messages.set(data.id, <Message>data);
break; break;
case "author": case "author":
datastore[data.adapter].profileCache.set(data.id, <Author>data); store.profileCache.set(data.id, <Author>data);
break; break;
default: default:
break; 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) => { _conn.addEventListener("error", (e: any) => {
console.log("websocket connection error"); console.log("websocket connection error");
console.log(JSON.stringify(e)); console.log(JSON.stringify(e));
}); });
_("websocket", _conn); DatagramSocket.conn = _conn;
}
} }
export default { connect }

View file

@ -2,7 +2,6 @@ package models
import ( import (
"encoding/json" "encoding/json"
"time"
) )
type Datagram struct { type Datagram struct {
@ -12,6 +11,8 @@ type Datagram struct {
Adapter string `json:"adapter"` Adapter string `json:"adapter"`
Type string `json:"type"` Type string `json:"type"`
Target *string `json:"target,omitempty"` Target *string `json:"target,omitempty"`
Created int64 `json:"created"`
Updated *int64 `json:"updated,omitempty"`
} }
type Message struct { type Message struct {
@ -23,8 +24,6 @@ type Message struct {
Replies []string `json:"replies"` Replies []string `json:"replies"`
ReplyCount int `json:"replyCount"` ReplyCount int `json:"replyCount"`
Mentions []string `json:"mentions"` Mentions []string `json:"mentions"`
Created time.Time `json:"created"`
Edited *time.Time `json:"edited,omitempty"`
Visibility string `json:"visibility"` Visibility string `json:"visibility"`
} }
@ -40,7 +39,7 @@ type Attachment struct {
Src string `json:"src"` Src string `json:"src"`
ThumbSrc string `json:"thumbSrc"` ThumbSrc string `json:"thumbSrc"`
Desc string `json:"desc"` Desc string `json:"desc"`
CreatedAt time.Time `json:"createdAt"` Created int64 `json:"created"`
Size uint64 `json:"size"` Size uint64 `json:"size"`
} }