use static classes for Settings and AdapterState
This commit is contained in:
parent
fead16168a
commit
2976d438d9
17 changed files with 262 additions and 140 deletions
10
README.md
10
README.md
|
@ -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`
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
3
build.sh
3
build.sh
|
@ -23,7 +23,6 @@ case "$1" in
|
|||
go build
|
||||
;;
|
||||
*)
|
||||
$0 client
|
||||
$0 server
|
||||
echo "usage: ${0} <front|server>"
|
||||
;;
|
||||
esac
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
@ -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
19
frontend/ts/settings.ts
Normal 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();
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue