redoing frontend stuff, we have a basic profile fetch working

This commit is contained in:
Iris Lightshard 2024-12-01 14:08:02 -07:00
parent 305366dc9e
commit 64d00083b7
Signed by: Iris Lightshard
GPG key ID: 688407174966CAF3
10 changed files with 135 additions and 50 deletions

View file

@ -137,7 +137,7 @@ func getBodyJson(res *http.Response) []byte {
l := res.ContentLength l := res.ContentLength
// 4k is a reasonable max size if we get unknown length right? // 4k is a reasonable max size if we get unknown length right?
if l < 0 { if l < 0 {
l = 4096 l = 4096
} }
jsonData := make([]byte, l) jsonData := make([]byte, l)
res.Body.Read(jsonData) res.Body.Read(jsonData)

View file

@ -36,13 +36,14 @@ func (self *HonkAdapter) Name() string {
} }
func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error { func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error {
self.data = data
// separate name and server in handle // separate name and server in handle
parts := strings.Split(*settings.Handle, "@") parts := strings.Split(*settings.Handle, "@")
self.username = parts[1] self.username = parts[1]
self.server = "https://" + parts[2] self.server = "https://" + parts[2]
self.nickname = settings.Nickname self.nickname = settings.Nickname
if settings.Password == nil { if settings.Password == nil || *settings.Password == "" {
// we're anonymous! // we're anonymous!
return nil return nil
} }
@ -71,10 +72,11 @@ func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error {
func (self *HonkAdapter) Subscribe(string) []error { func (self *HonkAdapter) Subscribe(string) []error {
// similar to the misskey adapter, we will poll for new honks and send them on the channel as they come in // similar to the misskey adapter, we will poll for new honks and send them on the channel as they come in
return nil
} }
func (self *HonkAdapter) Fetch(etype string, ids []string) error { func (self *HonkAdapter) Fetch(etype string, ids []string) error {
// honk API is limited, we fall back to the anonymous adapter for fetch ops // honk API is limited, we fall back to the anonymous adapter for fetch ops
aaa := anonAPAdapter{} aaa := anonAPAdapter{}
aaa.Init(self.data, self.server, "honk", self.nickname) aaa.Init(self.data, self.server, "honk", self.nickname)
return aaa.Fetch(etype, ids) return aaa.Fetch(etype, ids)

View file

@ -2,7 +2,7 @@
<html lang="en-US"> <html lang="en-US">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>underBBS</title> <title>underBBS test</title>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="shortcut icon" href="./favicon.png"/> <link rel="shortcut icon" href="./favicon.png"/>
<link href="./style.css" rel="stylesheet" /> <link href="./style.css" rel="stylesheet" />
@ -13,9 +13,10 @@
</div> </div>
</noscript> </noscript>
<div id="err_wrapper" style='display:none'><button id="err_close" onclick="closeErr()">x</button><div id="err_div"></div></div> <div id="err_wrapper" style='display:none'><button id="err_close" onclick="closeErr()">x</button><div id="err_div"></div></div>
<nav id="tabbar_injectparent"> <main>
</nav> <details open><summary>settings</summary><div id="settings_parent"></div></details>
<main id="mainarea_injectparent"> <details open><summary>profile</summary><div id="profile_parent"></div></details>
<details open><summary>honks</summary><div id="honks_parent"></div></details>
</main> </main>
</body> </body>
<script src="./main.js" type="application/javascript"></script> <script src="./main.js" type="application/javascript"></script>

View file

@ -3,6 +3,7 @@ export class BoostTileElement extends HTMLElement {
static observedAttributes = [ "data-boostid", "data-msgid", "data-author", "data-booster" ]; static observedAttributes = [ "data-boostid", "data-msgid", "data-author", "data-booster" ];
constructor() { constructor() {
super();
this.innerHTML = "<div class='boost_booster'></div><div class='boost_author'></div><div class='boost_content'></div>"; this.innerHTML = "<div class='boost_booster'></div><div class='boost_author'></div><div class='boost_content'></div>";
} }

View file

@ -7,9 +7,11 @@ import { MessageElement } from "./message-element"
import { SettingsElement } from "./settings-element" import { SettingsElement } from "./settings-element"
import { AdapterElement } from "./adapter-element" import { AdapterElement } from "./adapter-element"
import { ThreadSummaryElement } from "./thread-summary-element" import { ThreadSummaryElement } from "./thread-summary-element"
import { ProfileElement } from "./profile-element"
import {DatagramSocket} from "./websocket"
function main() { function main() {
const saveData = localStorage.getItem("settings"); const saveData = localStorage.getItem("underbbs_settings");
Settings._instance = saveData ? <Settings>JSON.parse(saveData) : new Settings(); Settings._instance = saveData ? <Settings>JSON.parse(saveData) : new Settings();
customElements.define("underbbs-tabbar", TabBarElement); customElements.define("underbbs-tabbar", TabBarElement);
@ -17,39 +19,17 @@ function main() {
customElements.define("underbbs-settings", SettingsElement); customElements.define("underbbs-settings", SettingsElement);
customElements.define("underbbs-adapter", AdapterElement); customElements.define("underbbs-adapter", AdapterElement);
customElements.define("underbbs-thread-summary", ThreadSummaryElement); customElements.define("underbbs-thread-summary", ThreadSummaryElement);
customElements.define("underbbs-profile", ProfileElement);
util._("closeErr", util.closeErr); util._("closeErr", util.closeErr);
let settingsParent = util.$("settings_parent");
tabbarInit(Settings._instance.adapters?.map(a=>a.nickname) ?? []); if (settingsParent) {
settingsParent.innerHTML = `<underbbs-settings data-adapters='${Settings._instance.adapters.map(a=>a.nickname).join(",")}'></underbbs-settings>`
registerServiceWorker();
}
function tabbarInit(adapters: string[]) {
const nav = util.$("tabbar_injectparent");
if (nav) {
nav.innerHTML = `<underbbs-tabbar data-adapters="" data-currentadapter=""></underbbs-tabbar>`;
} }
}
async function registerServiceWorker() { let profileParent = util.$("profile_parent");
if ("serviceWorker" in navigator) { if (profileParent) {
try { profileParent.innerHTML = "<underbbs-profile data-adapter='honk' data-target='https://cafe.nilfm.cc/u/nilix'></underbbs-profile>"
const registration = await navigator.serviceWorker.register("/serviceWorker.js", {
scope: "/",
});
if (registration.installing) {
console.log("Service worker installing");
} else if (registration.waiting) {
console.log("Service worker installed");
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Registration failed with ${error}`);
}
const registration = await navigator.serviceWorker.ready;
(registration as any).sync.register("testdata").then((r:any)=>{console.log("but i will see this!")});
} }
} }

View file

@ -0,0 +1,82 @@
import { Author } from "./message"
import util from "./util"
import { BatchTimer } from "./batch-timer"
import { AdapterState } from "./adapter"
export class ProfileElement extends HTMLElement {
static observedAttributes = [ "data-latest", "data-adapter", "data-target" ]
private _id: string | null = null;
private _adapter: string = "";
private _author: Author | null = null;
private _authorTimer: BatchTimer | null = null;
constructor() {
super();
this.innerHTML = "<div class='author_pfp'></div><div class='author_id'></div><div class='author_name'></div><div class='author_description'></div>"
}
connectedCallback() {
this._id = this.getAttribute("data-target");
this._adapter = this.getAttribute("data-adapter") ?? "";
const gateway = this.getAttribute("data-gateway") ?? "";
this._authorTimer = new BatchTimer((ids: string[])=>{
let url = `${gateway}/api/adapters/${this._adapter}/fetch?entity_type=author`;
for (let id of ids) {
url += `&entity_id=${id}`;
}
util.authorizedFetch("GET", url, null)
});
}
attributeChangedCallback(attr: string, prev: string, next: string) {
switch (attr) {
case "data-target":
if (!next) {
return
}
this._id = next;
if (this._authorTimer) {
this._authorTimer.queue(next, 100);
}
break;
case "data-latest":
let datastore = AdapterState._instance.data.get(this._adapter);
if (!datastore) {
console.log("no data yet, wait for some to come in maybe...");
return;
}
this._author = datastore.profileCache.get(next) ?? null;
console.log(this._author);
if (this._author == null) {
return;
}
if (this._author.id == next) {
let pfp = this.querySelector(".author_pfp");
let handle = this.querySelector(".author_id");
let prefName = this.querySelector(".author_name");
let bio = this.querySelector(".author_description");
if (pfp) {
pfp.innerHTML = `<img src="${this._author.profilePic}">`
}
if (handle) {
handle.innerHTML = `<a href="${this._author.uri}">${this._author.id}</a>`;
}
if (prefName) {
prefName.innerHTML = this._author.name;
}
if (bio) {
bio.innerHTML = this._author.profileData;
}
}
break;
}
}
}

View file

@ -58,7 +58,7 @@ export class SettingsElement extends HTMLElement {
return ()=>{ return ()=>{
// dropdown for protocol // dropdown for protocol
let html = "<select id='settings_newadapter_protocolselect'>"; let html = "<select id='settings_newadapter_protocolselect'>";
html += [ "nostr", "mastodon", "misskey" ].reduce((self, p)=>{ html += [ "nostr", "mastodon", "misskey", "honk" ].reduce((self, p)=>{
self += `<option value='${p}'>${p}</option>`; self += `<option value='${p}'>${p}</option>`;
return self; return self;
}, ""); }, "");
@ -114,6 +114,11 @@ export class SettingsElement extends HTMLElement {
const apiKey = (util.$("settings_newadapter_masto_apikey") as HTMLInputElement)?.value ?? ""; const apiKey = (util.$("settings_newadapter_masto_apikey") as HTMLInputElement)?.value ?? "";
adapterdata = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, server: server, apiKey: apiKey }; adapterdata = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, server: server, apiKey: apiKey };
break; break;
case "honk":
const handle = (util.$("settings_newadapter_honk_handle") as HTMLInputElement)?.value ?? "";
const password = (util.$("settings_newadapter_honk_password") as HTMLInputElement)?.value ?? "";
adapterdata = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, handle: handle, password: password };
break;
} }
const settings = Settings._instance; const settings = Settings._instance;
if (settings) { if (settings) {
@ -122,7 +127,7 @@ export class SettingsElement extends HTMLElement {
} }
settings.adapters.push(adapterdata); settings.adapters.push(adapterdata);
self._adapters.push(adapterdata.nickname); self._adapters.push(adapterdata.nickname);
localStorage.setItem("settings", JSON.stringify(settings)); localStorage.setItem("underbbs_settings", JSON.stringify(settings));
self.showSettings(self)(); self.showSettings(self)();
} }
@ -146,6 +151,10 @@ export class SettingsElement extends HTMLElement {
html += " <label>server<input id='settings_newadapter_masto_server'/></label>"; html += " <label>server<input id='settings_newadapter_masto_server'/></label>";
html += " <label>API key<input id='settings_newadapter_masto_apikey'/></label>"; html += " <label>API key<input id='settings_newadapter_masto_apikey'/></label>";
break; break;
case "honk":
html += " <label>nickname<input id='settings_newadapter_nickname'/></label>";
html += " <label>handle<input id='settings_newadapter_honk_handle'/></label>";
html += " <label>password<input id='settings_newadapter_honk_password'/></label>";
} }
const div = util.$("settings_newadapter_protocoloptions"); const div = util.$("settings_newadapter_protocoloptions");

View file

@ -10,6 +10,10 @@ export class AdapterConfig {
// nostr // nostr
public privkey: string | null = null; public privkey: string | null = null;
public relays: string[] | null = null; public relays: string[] | null = null;
// honk
public handle: string | null = null;
public password: string | null = null;
} }
export class Settings { export class Settings {

View file

@ -22,10 +22,12 @@ export class DatagramSocket {
util.authorizedFetch("POST", "/api/adapters", JSON.stringify(Settings._instance.adapters)) util.authorizedFetch("POST", "/api/adapters", JSON.stringify(Settings._instance.adapters))
.then(r=> { .then(r=> {
if (r.ok) { if (r.ok) {
const tabbar = document.querySelector("underbbs-tabbar"); // iterate through any components which might want to fetch data
if (tabbar) { const profiles = document.querySelectorAll("underbbs-profile");
tabbar.setAttribute("data-adapters", Settings._instance.adapters.map(a=>a.nickname).join(",")); profiles.forEach(p=>{
} const target = p.getAttribute("data-target");
p.setAttribute("data-target", target ?? "");
});
} }
}) })
.catch(e => { .catch(e => {
@ -36,7 +38,8 @@ export class DatagramSocket {
if (!store) { if (!store) {
AdapterState._instance.data.set(data.adapter, new AdapterData(data.protocol)); AdapterState._instance.data.set(data.adapter, new AdapterData(data.protocol));
store = AdapterState._instance.data.get(data.adapter); store = AdapterState._instance.data.get(data.adapter);
} else { }
if (store) {
// typeswitch on the incoming data type and fill the memory // typeswitch on the incoming data type and fill the memory
switch (data.type) { switch (data.type) {
case "message": case "message":
@ -49,11 +52,12 @@ export class DatagramSocket {
break; break;
} }
} }
// if the adapter is active signal it that there's new data console.log(store);
let adapter = util.$(`adapter_${data.adapter}`); // now search for any components with this adapter and target
if (adapter) { let targets = document.querySelectorAll(`*[data-adapter="${data.adapter}"][data-target="${data.id}"]`);
adapter.setAttribute("data-latest", data.id); targets.forEach(t=>{
} t.setAttribute("data-latest", data.id);
});
} }
} }

View file

@ -80,6 +80,8 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
a = &adapter.MastoAdapter{} a = &adapter.MastoAdapter{}
case "misskey": case "misskey":
a = &adapter.MisskeyAdapter{} a = &adapter.MisskeyAdapter{}
case "honk":
a = &adapter.HonkAdapter{}
default: default:
break break