diff --git a/README.md b/README.md index 106cac7..3ed5a77 100644 --- a/README.md +++ b/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 \ No newline at end of file +from the project root: + +1. `./build.sh front` +2. `./build.sh server` +3. `./underbbs` + +visit `http://localhost:9090/app` \ No newline at end of file diff --git a/adapter/mastodon.go b/adapter/mastodon.go index 5923c73..636ed5d 100644 --- a/adapter/mastodon.go +++ b/adapter/mastodon.go @@ -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 { diff --git a/adapter/misskey.go b/adapter/misskey.go index 3e2e96f..49d38f4 100644 --- a/adapter/misskey.go +++ b/adapter/misskey.go @@ -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, diff --git a/adapter/nostr.go b/adapter/nostr.go index 0906d58..66157a6 100644 --- a/adapter/nostr.go +++ b/adapter/nostr.go @@ -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: diff --git a/build.sh b/build.sh index ca76d27..6c0902a 100755 --- a/build.sh +++ b/build.sh @@ -23,7 +23,6 @@ case "$1" in go build ;; *) - $0 client - $0 server + echo "usage: ${0} " ;; esac diff --git a/frontend/ts/adapter-element.ts b/frontend/ts/adapter-element.ts index 0714cc2..93e8bff 100644 --- a/frontend/ts/adapter-element.ts +++ b/frontend/ts/adapter-element.ts @@ -1,9 +1,8 @@ import util from "./util" + import { Message, Author } from "./message" import { MessageThread } from "./thread" - -var _ = util._ -var $ = util.$ +import { AdapterState } from "./adapter" export class AdapterElement extends HTMLElement { static observedAttributes = [ "data-latest", "data-view", "data-viewing" ] @@ -30,14 +29,17 @@ 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 +=`
  • `; + for (const t of this._threads.sort((a: MessageThread, b: MessageThread) => b.latest - a.latest)) { + html +=`
  • `; } pl.innerHTML = html; } } updateIdxView(latest: string, rootId: string) { - const existingThread = this.querySelector("underbbs-thread-summary[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(`
  • `); + const li = document.createElement("li"); + li.innerHTML = ``; + let nextThread: Element | null = null; + for (let i = 0; i < pl.children.length; i++) { + const c = pl.children.item(i); + const latest = c?.children.item(0)?.getAttribute("data-latest") + if (latest && parseInt(latest) < thread.latest) { + nextThread = c; + break; + } + } + if (nextThread) { + nextThread.insertAdjacentElement('beforebegin', li) + return + } + pl.append(li); } } } @@ -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); } } diff --git a/frontend/ts/adapter.ts b/frontend/ts/adapter.ts index 2055854..85dd63d 100644 --- a/frontend/ts/adapter.ts +++ b/frontend/ts/adapter.ts @@ -13,6 +13,8 @@ export class AdapterData { } } -export interface AdapterState { - [nickname: string]: AdapterData; -} \ No newline at end of file +export class AdapterState { + public data: Map = new Map(); + + static _instance: AdapterState = new AdapterState(); +} diff --git a/frontend/ts/index.ts b/frontend/ts/index.ts index 398fef6..48c4043 100644 --- a/frontend/ts/index.ts +++ b/frontend/ts/index.ts @@ -1,33 +1,33 @@ +import util from "./util" import {AdapterState, AdapterData} from "./adapter"; import {Message, Attachment, Author} from "./message" -import util from "./util" +import {Settings} from "./settings" import { TabBarElement } from "./tabbar-element" import { MessageElement } from "./message-element" import { SettingsElement } from "./settings-element" import { AdapterElement } from "./adapter-element" import { ThreadSummaryElement } from "./thread-summary-element" -var $ = util.$ -var _ = util._ - function main() { - const settings = _("settings", JSON.parse(localStorage.getItem("settings") ?? "{}")); + Settings._instance = 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 = ``; + nav.innerHTML = ``; } } diff --git a/frontend/ts/message.ts b/frontend/ts/message.ts index dc2d799..e7dc2f0 100644 --- a/frontend/ts/message.ts +++ b/frontend/ts/message.ts @@ -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"; } diff --git a/frontend/ts/settings-element.ts b/frontend/ts/settings-element.ts index 23862d9..e59340b 100644 --- a/frontend/ts/settings-element.ts +++ b/frontend/ts/settings-element.ts @@ -1,8 +1,7 @@ import util from "./util" -import websocket from "./websocket" +import {DatagramSocket} from "./websocket" +import {Settings} from "./settings" -var $ = util.$ -var _ = util._ export class SettingsElement extends HTMLElement { static observedAttributes = [ "data-adapters" ] @@ -18,7 +17,12 @@ 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 += ""; 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; } diff --git a/frontend/ts/settings.ts b/frontend/ts/settings.ts new file mode 100644 index 0000000..24a1aad --- /dev/null +++ b/frontend/ts/settings.ts @@ -0,0 +1,19 @@ +export class AdapterConfig { + // common + public nickname: string = ""; + public protocol: string = ""; + + // masto/misskey + public server: string | null = null; + public apiKey: string | null = null; + + // nostr + public privkey: string | null = null; + public relays: string[] | null = null; +} + +export class Settings { + public adapters: AdapterConfig[] = []; + + static _instance: Settings = new Settings(); +} \ No newline at end of file diff --git a/frontend/ts/tabbar-element.ts b/frontend/ts/tabbar-element.ts index c921d29..2597029 100644 --- a/frontend/ts/tabbar-element.ts +++ b/frontend/ts/tabbar-element.ts @@ -1,6 +1,5 @@ import util from "./util" -var _ = util._ -var $ = util.$ +import {Settings} from "./settings" export class TabBarElement extends HTMLElement { static observedAttributes = [ "data-adapters", "data-currentadapter" ] @@ -24,7 +23,7 @@ export class TabBarElement extends HTMLElement { attributeChangedCallback() { let html = "
    • settings
    • "; - 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 = ``; + x.innerHTML = `a.nickname).join(",") ?? []}>`; 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 = ``; self.setAttribute("data-currentadapter", adapter); diff --git a/frontend/ts/thread-summary-element.ts b/frontend/ts/thread-summary-element.ts index 1eea87b..ba33a0d 100644 --- a/frontend/ts/thread-summary-element.ts +++ b/frontend/ts/thread-summary-element.ts @@ -1,7 +1,6 @@ import util from "./util" import { Message, Author } from "./message" -var _ = util._ -var $ = util.$ +import { AdapterState } from "./adapter" export class ThreadSummaryElement extends HTMLElement { static observedAttributes = [ "data-len", "data-author", "data-latest", "data-new" ]; @@ -10,8 +9,8 @@ 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 - ? `${this._author.id} ${this._author.id}` + : `${this._author.id}` ; } } } @@ -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); diff --git a/frontend/ts/thread.ts b/frontend/ts/thread.ts index 143f88b..e1c5247 100644 --- a/frontend/ts/thread.ts +++ b/frontend/ts/thread.ts @@ -1,7 +1,5 @@ import util from "./util" import { Message } from "./message" -var _ = util._ -var $ = util.$ export class MessageNode { public parent: MessageNode | null = null; @@ -27,8 +25,8 @@ export class MessageThread { public root: MessageNode; public messageCount: number; public visibility: string; - public created: Date; - public latest: Date; + public created: number; + public latest: number; constructor(first: Message) { this.root = new MessageNode(first); @@ -44,7 +42,7 @@ 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; } } diff --git a/frontend/ts/util.ts b/frontend/ts/util.ts index cbe5f4c..365a9d5 100644 --- a/frontend/ts/util.ts +++ b/frontend/ts/util.ts @@ -1,3 +1,4 @@ +import { DatagramSocket } from './websocket' function _(key: string, value: any | null | undefined = undefined): any | null { const x = 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 { 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 } \ No newline at end of file +export default { _, $, authorizedFetch, errMsg, closeErr } \ No newline at end of file diff --git a/frontend/ts/websocket.ts b/frontend/ts/websocket.ts index 1c0d180..f510aa3 100644 --- a/frontend/ts/websocket.ts +++ b/frontend/ts/websocket.ts @@ -1,54 +1,75 @@ import util from "./util" import {AdapterState, AdapterData} from "./adapter"; import {Message, Attachment, Author} from "./message" +import {Settings} from "./settings" -var $ = util.$ -var _ = util._ -function connect() { +export class DatagramSocket { + public static skey: string | null = null; + public static conn: WebSocket | null; - let datastore = _("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, data); + store.messages.set(data.id, data); break; case "author": - datastore[data.adapter].profileCache.set(data.id, data); + store.profileCache.set(data.id, 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 } \ No newline at end of file diff --git a/models/msg.go b/models/msg.go index ec0ad9d..bfa2267 100644 --- a/models/msg.go +++ b/models/msg.go @@ -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"` }