all repos — underbbs @ c39c9f9412c817ee526926468d9b3d3dbe6d1663

decentralized social media client

convert tabbar and settings to web components
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iHUEABYKAB0WIQT/foVVmI9pK13hPWFohAcXSWbK8wUCZoInvQAKCRBohAcXSWbK
82oRAP9oIVL2UgDenYk790J06KHnl57velZHr8IykLVZIzu2lgD/b2JLhShVg9ga
J7bqToQ05FuLuDxj3AEJBcanwfKzmwE=
=r6jM
-----END PGP SIGNATURE-----
commit

c39c9f9412c817ee526926468d9b3d3dbe6d1663

parent

b8429533df17831ce4f23398d735f1cd97475fd5

M dist/index.htmldist/index.html

@@ -12,19 +12,10 @@ <noscript><div id="noscript_container">

JS app lol </div> </noscript> - <div id="map"></div> - <div id="err_wrapper" style='display:none'><button id="err_close" onclick="closeErr()">x</button><div id="err_div"></div></div> - - <nav> - <ul> - <li><a href="#" onclick="showSettings()">settings</a></li> - </ul> + <nav id="tabbar_injectparent"> </nav> - <main> - <button id="connectbtn" onclick="connect()">Connect</button> - <div id="tabbar"></div> - <div id="tabcontent"></div> + <main id="mainarea_injectparent"> </main> </body> <script src="./main.js" type="application/javascript"></script>
M dist/style.cssdist/style.css

@@ -42,4 +42,26 @@ }

.err { color: var(--err_color); +} + +nav ul li { + display: inline; + padding: 0.5em; +} + +nav { + padding: 1em; +} + +nav ul li a { + text-decoration: none; + border-bottom: solid 1px var(--bg_color); +} + +.tabbar_current { + border-bottom: solid 1px var(--main_color); +} + +main { + padding: 2em; }
M ts/adapter.tsts/adapter.ts

@@ -1,14 +1,15 @@

import {Message, Author} from "./message" export class AdapterData { public protocol: string; - public directMessages Map<string, Message>(); - public messages: Map<string, Message>(); + public directMessages: Map<string, Message>; + public messages: Map<string, Message>; public profileCache: Map<string, Author>; constructor(protocol: string) { this.protocol = protocol; - this.messages = []; - this.profileCache = []; + this.messages = new Map<string, Message>(); + this.directMessages = new Map<string, Message>(); + this.profileCache = new Map<string, Author>(); } }
M ts/index.tsts/index.ts

@@ -1,29 +1,31 @@

-import {Adapter} from "./adapter"; -import {Message, Attachment} from "./message" +import {AdapterState, AdapterData} from "./adapter"; +import {Message, Attachment, Author} from "./message" import util from "./util" +import { TabBarElement } from "./tabbar-element" +import { MessageElement } from "./message-element" +import { SettingsElement } from "./settings-element" var $ = util.$ var _ = util._ -function main():void { +function main() { const settings = _("settings", JSON.parse(localStorage.getItem("settings") ?? "{}")); - const adapters = _("adapters", []); - if (settings != null) { - for (let s of settings.adapters ?? []) { - - } - if (adapters.length > 0) { - _("currentAdapter", adapters[0].nickname); - // update tabbar and tabcontent with first adapter - } - } else { - console.log("no settings exist for this client"); - _("settings", { adapters: [] }); - showSettings(); - } + customElements.define("underbbs-tabbar", TabBarElement); + customElements.define("underbbs-message", MessageElement); + customElements.define("underbbs-settings", SettingsElement); + + tabbarInit(settings.adapters?.map((a:any)=>a.nickname) ?? []); + registerServiceWorker(); -}; +} + +function tabbarInit(adapters: string[]) { + const nav = $("tabbar_injectparent"); + if (nav) { + nav.innerHTML = `<underbbs-tabbar data-adapters="${adapters.join(",")}" data-currentadapter=""></underbbs-tabbar>`; + } +} async function registerServiceWorker() { if ("serviceWorker" in navigator) {

@@ -44,207 +46,6 @@ }

const registration = await navigator.serviceWorker.ready; (registration as any).sync.register("testdata").then((r:any)=>{console.log("but i will see this!")}); } -}; - - -function showSettings():void { - // tab bar hidden - const tabbar = $("tabbar"); - if (tabbar) { - tabbar.style.display = "none"; - } - - // tabcontent to show settings ui - const tabcontent = $("tabcontent"); - const adapters = _("adapters") as Adapter[] ?? []; - - if (tabcontent) { - let html = "<p>this is our settings dialogue</p>"; - html += "<button onclick='addAdapter()'>New</button>"; - html += adapters.reduce((self: string, a: Adapter) => { - self += `<li><a href='#' onclick='editAdapter(${a.nickname})'>${a.nickname}</a></li>` - return self; - }, "<ul id='settings_adapterlist'>"); - html += "</ul>"; - html += "<button onclick='saveSettings()'>save</button>"; - tabcontent.innerHTML = html; - } } -function addAdapter(): void { - const tabcontent = $("tabcontent"); - if (tabcontent) { - // dropdown for protocol - let html = "<select id='settings_newadapter_protocolselect' onchange='fillAdapterProtocolOptions()'>"; - html += [ "nostr", "mastodon", "misskey" ].reduce((self, p)=>{ - self += `<option value='${p}'>${p}</option>`; - return self; - }, ""); - html += "</select>"; - - // nostr is the first protocol, so show its options by default - html += "<div id='settings_newadapter_protocoloptions'>"; - html += " <label>nickname<input id='settings_newadapter_nickname'/></label>"; - html += " <label>privkey<input id='settings_newadapter_nostr_privkey'/></label>"; - html += " <label>default relays<input id='settings_newadapter_nostr_default_relays'/></label>"; - html += "</div>"; - - html += "<button onclick='saveAdapter()'>Add</button>"; - html += "<button onclick='showSettings()'>Back</button>"; - - tabcontent.innerHTML = html; - } -} - -function fillAdapterProtocolOptions(): void { - const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; - - let html = ""; - - switch(proto?.options[proto.selectedIndex].value) { - case "nostr": - html += " <label>nickname<input id='settings_newadapter_nickname'/></label>"; - html += " <label>privkey<input id='settings_newadapter_nostr_privkey'/></label>"; - html += " <label>default relays<input id='settings_newadapter_nostr_default_relays'/></label>"; - break; - case "mastodon": - case "misskey": - html += " <label>nickname<input id='settings_newadapter_nickname'/></label>"; - html += " <label>server<input id='settings_newadapter_masto_server'/></label>"; - html += " <label>API key<input id='settings_newadapter_masto_apikey'/></label>"; - break; - } - - - const div = $("settings_newadapter_protocoloptions"); - if (div) { - div.innerHTML = html; - } -} - -function saveSettings(): void { - const settings = _("settings"); - if (settings) { - localStorage.setItem("settings", JSON.stringify(settings)); - } - // tab bar hidden - const tabbar = $("tabbar"); - if (tabbar) { - tabbar.style.display = "block"; - } - - // tabcontent to show settings ui - const tabcontent = $("tabcontent"); - if (tabcontent) { - tabcontent.innerHTML = ""; - } -} - -function saveAdapter(): void { - let self: any = {}; - // get selected adapter protocol - const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; - console.log(proto.options[proto.selectedIndex]); - - - const nickname = ($("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 ?? ""; - self = { 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 ?? ""; - self = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, server: server, apiKey: apiKey }; - break; - } - const settings = _("settings"); - if (settings) { - if (!settings.adapters) { - settings.adapters = []; - } - settings.adapters.push(self); - - localStorage.setItem("settings", JSON.stringify(settings)); - showSettings(); - } -} - - -let _conn: WebSocket | null = null; - -async function authorizedFetch(method: string, uri: string, body: any): Promise<Response> { - const headers = new Headers() - headers.set('Authorization', 'Bearer ' + _("skey")) - return await fetch(uri, { - method: method, - headers: headers, - body: body, - }) -} - -function connect() { - - var datastore: AdapterState = {} - datastore = _("datastore", datastore); - - const wsProto = location.protocol == "https:" ? "wss" : "ws"; - _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) => { - - // debugging - console.log(e) - - // now we'll figure out what to do with it - const data = JSON.parse(e.data); - if (data.key) { - _("skey", data.key) - authorizedFetch("POST", "/api/adapters", JSON.stringify(_("settings").adapters)) - } else { - if (!datastore[data.adapter]) { - datastore[data.adapter] = new AdapterData(data.protocol); - } - - // typeswitch on the incoming data type and fill the memory - switch (data.type) { - case "message": - datastore[data.adapter].messages[data.id] = <Message>data; - break; - case "author": - datastore[data.adapter].profileCache[data.id] = <Author>data; - break; - default: - break; - } - // if the adapter is active, inject the web components - // FOR HOTFETCHED DATA: - // before fetching, we can set properties on the DOM, - // so when those data return to us we know where to inject components! - if (_("currentAdapter") == data.adapter) { - // dive in and insert that shit in the dom - } - } - }); - _conn.addEventListener("error", (e: any) => { - console.log("websocket connection error"); - console.log(JSON.stringify(e)); - }); - _("websocket", _conn); -} - -_("addAdapter", addAdapter); -_("saveAdapter", saveAdapter); -_("fillAdapterProtocolOptions", fillAdapterProtocolOptions); -_("showSettings", showSettings); -_("saveSettings", saveSettings); -_("connect", connect); main();
A ts/settings-element.ts

@@ -0,0 +1,160 @@

+import util from "./util" +import websocket from "./websocket" + +var $ = util.$ +var _ = util._ + +export class SettingsElement extends HTMLElement { + static observedAttributes = [ "data-adapters" ] + + private _adapters: string[] = []; + + constructor() { + super(); + } + + connectedCallback() { + this.attributeChangedCallback(); + } + + attributeChangedCallback() { + this._adapters = this.getAttribute("data-adapters")?.split(",") ?? []; + this.showSettings(this)(); + } + + showSettings(self: SettingsElement): ()=>void { + return ()=>{ + let html = ""; + html += "<button id='settings_adapter_create_btn'>New</button>"; + html += self._adapters.reduce((self: string, a: string) => { + self += `<li><a id='settings_adapter_edit_${a}' href='#editadapter'>${a}</a></li>` + return self; + }, "<ul id='settings_adapterlist'>"); + html += "</ul>"; + html += "<button id='settings_save_btn'>save</button>"; + self.innerHTML = html; + + let create = $("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}`); + if (edit) { + edit.addEventListener("click", self.showEditAdapterFunc(a, self), false); + } + } + let connect = $("settings_connect_btn"); + if (connect) { + connect.addEventListener("click", websocket.connect, false); + } + } + } + + showCreateAdapter(self: SettingsElement): ()=>void { + return ()=>{ + // dropdown for protocol + let html = "<select id='settings_newadapter_protocolselect'>"; + html += [ "nostr", "mastodon", "misskey" ].reduce((self, p)=>{ + self += `<option value='${p}'>${p}</option>`; + return self; + }, ""); + html += "</select>"; + + // nostr is the first protocol, so show its options by default + html += "<div id='settings_newadapter_protocoloptions'>"; + html += " <label>nickname<input id='settings_newadapter_nickname'/></label>"; + html += " <label>privkey<input id='settings_newadapter_nostr_privkey'/></label>"; + html += " <label>default relays<input id='settings_newadapter_nostr_default_relays'/></label>"; + html += "</div>"; + + html += "<button id='settings_adapter_create_save_btn'>add</button>"; + html += "<button id='settings_adapter_create_back_btn'>back</button>"; + + self.innerHTML = html; + + let protocolSelect = $("settings_newadapter_protocolselect"); + if (protocolSelect) { + protocolSelect.addEventListener("change", self.fillAdapterProtocolOptions, false); + } + + let save = $("settings_adapter_create_save_btn"); + if (save) { + save.addEventListener("click", self.saveAdapter(self), false); + } + + let back = $("settings_adapter_create_back_btn"); + if (back) { + back.addEventListener("click", self.showSettings(self), false); + } + } + } + + saveAdapter(self: SettingsElement): ()=>void { + return ()=>{ + let adapterdata: any = {}; + // get selected adapter protocol + const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; + console.log(proto.options[proto.selectedIndex]); + + const nickname = ($("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 ?? ""; + 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 ?? ""; + adapterdata = { nickname: nickname, protocol: proto.options[proto.selectedIndex].value, server: server, apiKey: apiKey }; + break; + } + const settings = _("settings"); + if (settings) { + if (!settings.adapters) { + settings.adapters = []; + } + settings.adapters.push(adapterdata); + self._adapters.push(adapterdata.nickname); + localStorage.setItem("settings", JSON.stringify(settings)); + + self.setAttribute("adapters", self._adapters.join(",")); + } + } + } + + fillAdapterProtocolOptions() { + const proto = $("settings_newadapter_protocolselect") as HTMLSelectElement; + + let html = ""; + + switch(proto?.options[proto.selectedIndex].value) { + case "nostr": + html += " <label>nickname<input id='settings_newadapter_nickname'/></label>"; + html += " <label>privkey<input id='settings_newadapter_nostr_privkey'/></label>"; + html += " <label>default relays<input id='settings_newadapter_nostr_default_relays'/></label>"; + break; + case "mastodon": + case "misskey": + html += " <label>nickname<input id='settings_newadapter_nickname'/></label>"; + html += " <label>server<input id='settings_newadapter_masto_server'/></label>"; + html += " <label>API key<input id='settings_newadapter_masto_apikey'/></label>"; + break; + } + + const div = $("settings_newadapter_protocoloptions"); + if (div) { + div.innerHTML = html; + } + } + + + showEditAdapterFunc(adapter: string, self: SettingsElement): ()=>void { + // this UI has to be able to edit an exiting adapter or delete it + return ()=>{}; + } +}
A ts/tabbar-element.ts

@@ -0,0 +1,84 @@

+import util from "./util" +var _ = util._ +var $ = util.$ + +export class TabBarElement extends HTMLElement { + static observedAttributes = [ "data-adapters", "data-currentadapter" ] + + private _adapters: string[] | null = null; + private _currentAdapter: string | null = null; + + constructor() { + super(); + } + + connectedCallback() { + this.attributeChangedCallback(); + + if (this._currentAdapter) { + this.showAdapterFunc(this, this._currentAdapter)(); + } else { + this.showSettings(this)(); + } + } + + attributeChangedCallback() { + let html = "<ul><li><a id='tabbar_settings' href='#settings'>settings</a></li>"; + if (this.getAttribute("data-adapters") == "") { + this._adapters = []; + } else { + this._adapters = this.getAttribute("data-adapters")?.split(",") ?? []; + } + + this._currentAdapter = this.getAttribute("data-currentadapter"); + if (this._currentAdapter == "") { + this._currentAdapter = null; + } + + html = this._adapters.reduce((self: string, a: string)=>{ + self += `<li><a id="tabbar_${a}" href="#${a}">${a}</a></li>`; + return self; + }, html); + html += "</ul>"; + + this.innerHTML = html; + // now we can query the child elements and add click handlers to them + var s = $("tabbar_settings"); + if (s) { + s.addEventListener("click", this.showSettings(this), false); + if (!this._currentAdapter) { + s.classList.add("tabbar_current"); + } + } + for (let i of this._adapters) { + var a = $(`tabbar_${i}`); + if (a) { + a.addEventListener("click", this.showAdapterFunc(this, i), false); + if (this._currentAdapter == i) { + a.classList.add("tabbar_current"); + } + } + } + + } + + showSettings(self: TabBarElement): ()=>void { + return () => { + let x = $("mainarea_injectparent"); + if (x) { + x.innerHTML = `<underbbs-settings data-adapters=${self._adapters?.join(",") ?? []}></underbbs-settings>`; + self.setAttribute("data-currentadapter", ""); + } + } + } + + showAdapterFunc(self: TabBarElement, adapter: string): ()=>void { + return ()=>{ + let x = $("mainarea_injectparent"); + if (x) { + x.innerHTML = `<underbbs-adapter data-name="${adapter}"></underbbs-adapter>`; + self.setAttribute("data-currentadapter", adapter); + } + } + } +}
M ts/util.tsts/util.ts

@@ -11,4 +11,14 @@ function $(id: string): HTMLElement | null {

return document.getElementById(id); } -export default { _, $ }+async function authorizedFetch(method: string, uri: string, body: any): Promise<Response> { + const headers = new Headers() + headers.set('Authorization', 'Bearer ' + _("skey")) + return await fetch(uri, { + method: method, + headers: headers, + body: body, + }) +} + +export default { _, $, authorizedFetch }
M ts/websocket.tsts/websocket.ts

@@ -0,0 +1,61 @@

+import util from "./util" +import {AdapterState, AdapterData} from "./adapter"; +import {Message, Attachment, Author} from "./message" + +var $ = util.$ +var _ = util._ + +function connect() { + + var datastore: AdapterState = {} + datastore = _("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) => { + + // debugging + console.log(e) + + // now we'll figure out what to do with it + const data = JSON.parse(e.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); + } + + // typeswitch on the incoming data type and fill the memory + switch (data.type) { + case "message": + datastore[data.adapter].messages.set(data.id, <Message>data); + break; + case "author": + datastore[data.adapter].profileCache.set(data.id, <Author>data); + break; + default: + break; + } + // if the adapter is active, inject the web components + // FOR HOTFETCHED DATA: + // before fetching, we can set properties on the DOM, + // so when those data return to us we know where to inject components! + if (_("currentAdapter") == data.adapter) { + // dive in and insert that shit in the dom + } + } + }); + _conn.addEventListener("error", (e: any) => { + console.log("websocket connection error"); + console.log(JSON.stringify(e)); + }); + _("websocket", _conn); +} + +export default { connect }
M tsconfig.jsontsconfig.json

@@ -1,6 +1,6 @@

{ "compilerOptions": { - "target": "es5", + "target": "es6", "lib": [ "es2022", "webworker", "dom" ], "skipLibCheck": true, "module": "preserve",