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
## 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`

View file

@ -110,10 +110,10 @@ func (self *MastoAdapter) mastoUpdateToMessage(status madon.Status) *Message {
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 {

View file

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

View file

@ -106,7 +106,7 @@ func (self *NostrAdapter) nostrEventToMsg(evt *nostr.Event) (Message, error) {
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:

View file

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

View file

@ -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 @@ export class AdapterElement extends HTMLElement {
}
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 @@ export class AdapterElement extends HTMLElement {
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 @@ export class AdapterElement extends HTMLElement {
}
// 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 @@ export class AdapterElement extends HTMLElement {
}
}
} 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 @@ export class AdapterElement extends HTMLElement {
}
setProfileView() {
let profile_bar = $("profile_bar");
let profile_bar = util.$("profile_bar");
if (profile_bar) {
// clear any previous data
} else {
@ -124,28 +146,44 @@ export class AdapterElement extends HTMLElement {
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 @@ export class AdapterElement extends HTMLElement {
}
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 @@ export class AdapterElement extends HTMLElement {
}
} 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 @@ export class AdapterElement extends HTMLElement {
// 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 @@ export class AdapterElement extends HTMLElement {
if (this.placeMsg(orphanedParent.id)) {
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 {
[nickname: string]: AdapterData;
}
export class AdapterState {
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 {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>`;
}
}

View file

@ -9,8 +9,8 @@ export class Message {
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";
}

View file

@ -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 @@ export class SettingsElement extends HTMLElement {
}
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 @@ export class SettingsElement extends HTMLElement {
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 @@ export class SettingsElement extends HTMLElement {
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 @@ export class SettingsElement extends HTMLElement {
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 @@ export class SettingsElement extends HTMLElement {
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 @@ export class SettingsElement extends HTMLElement {
break;
}
const div = $("settings_newadapter_protocoloptions");
const div = util.$("settings_newadapter_protocoloptions");
if (div) {
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"
var _ = util._
var $ = util.$
import {Settings} from "./settings"
export class TabBarElement extends HTMLElement {
static observedAttributes = [ "data-adapters", "data-currentadapter" ]
@ -24,7 +23,7 @@ export class TabBarElement extends HTMLElement {
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 @@ export class TabBarElement extends HTMLElement {
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 @@ export class TabBarElement extends HTMLElement {
}
}
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 @@ export class TabBarElement extends HTMLElement {
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 @@ export class TabBarElement extends HTMLElement {
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);

View file

@ -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 @@ export class ThreadSummaryElement extends HTMLElement {
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 @@ export class ThreadSummaryElement extends HTMLElement {
}
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 @@ export class ThreadSummaryElement extends HTMLElement {
// 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 @@ export class ThreadSummaryElement extends HTMLElement {
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 @@ export class ThreadSummaryElement extends HTMLElement {
}
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);

View file

@ -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 @@ export class MessageThread {
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 @@ export class MessageThread {
} else {
for (let n of node.children) {
const x = this.findNode(n, id);
if (x != null) {
if (x) {
return x;
}
}

View file

@ -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 @@ 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 {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 }

View file

@ -2,7 +2,6 @@ package models
import (
"encoding/json"
"time"
)
type Datagram struct {
@ -12,6 +11,8 @@ type Datagram struct {
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 @@ type Message struct {
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"`
}