From 1806140f4be9ef18bad8c38402e4448a9e201112 Mon Sep 17 00:00:00 2001 From: Iris Lightshard Date: Wed, 11 Dec 2024 20:19:11 -0700 Subject: [PATCH] merge author-messages and timeline components, start implementing navigator for viewing individual messages and authors --- adapter/anonAp.go | 18 +++- frontend/dist/style.css | 49 +++------- frontend/ts/author-messages-element.ts | 92 ------------------ frontend/ts/{batch-timer.ts => fetcher.ts} | 2 +- frontend/ts/message-element.ts | 50 ++++++++-- frontend/ts/navigator.ts | 108 +++++++++++++++++++++ frontend/ts/profile-element.ts | 10 +- frontend/ts/timeline-element.ts | 24 ++++- frontend/ts/util.ts | 1 - frontend/ts/websocket.ts | 9 +- 10 files changed, 207 insertions(+), 156 deletions(-) delete mode 100644 frontend/ts/author-messages-element.ts rename frontend/ts/{batch-timer.ts => fetcher.ts} (97%) create mode 100644 frontend/ts/navigator.ts diff --git a/adapter/anonAp.go b/adapter/anonAp.go index 5acc239..4098efd 100644 --- a/adapter/anonAp.go +++ b/adapter/anonAp.go @@ -225,11 +225,27 @@ func (self *anonAPAdapter) Fetch(etype string, ids []string) error { if string([]byte{id[0]}) == "@" { id = id[1:] } - res, err := http.Get(self.server + "/.well-known/webfinger?resource=acct:" + id) + reqHost := self.server + if strings.HasPrefix(id, "https://") || !strings.HasSuffix(id, strings.Split(self.server, "https://")[1]) { + if strings.Contains(id, "@") { + reqHost = "https://" + strings.Split(id, "@")[1] + id = strings.Split(id, "@")[0] + } else { + noScheme := strings.TrimPrefix(id, "https://") + domainOnly := strings.Split(noScheme, "/")[0] + reqHost = "https://" + domainOnly + idParts := strings.Split(id, "/") + id = idParts[len(idParts)-1] + } + } + fmt.Println(reqHost) + res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + id) if err != nil { return err } + fmt.Printf("%d\n", res.StatusCode) data := getBodyJson(res) + fmt.Println(string(data)) wf := webFinger{} json.Unmarshal(data, &wf) var profile string diff --git a/frontend/dist/style.css b/frontend/dist/style.css index 7ee7d6a..d33cf9c 100644 --- a/frontend/dist/style.css +++ b/frontend/dist/style.css @@ -1,11 +1,3 @@ -:root { - --bg_color: #000000; - --fg_color: #ccc; - --main_color: #1f9b92; - --sub_color: #002b36; - --err_color: #DC143C; -} - * { box-sizing: border-box; padding: 0; @@ -17,17 +9,6 @@ background: var(--bg_color); } -* { scrollbar-color:var(--main_color) var(--sub_color); } -*::-webkit-scrollbar { width:6px;height:6px; } -*::-webkit-scrollbar-track { background: var(--sub_color);} -*::-webkit-scrollbar-thumb { background:var(--main_color);border-radius:0;border:none; } -*::-webkit-scrollbar-corner { background:var(--sub_color); } -*::selection { background-color:var(--main_color);color:var(--bg_color);text-decoration:none;text-shadow:none; } - -body { - -} - a { color: var(--main_color); } @@ -44,27 +25,23 @@ input { color: var(--err_color); } -nav ul li { - display: inline; - padding: 0.5em; +underbbs-message, underbbs-profile { + max-width: 70ch; + display: block; +} + +underbbs-message img { + max-width: 100%; +} + +underbbs-profile img { + max-width: 200px; } 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; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto; +underbbs-message .message_metadata span { + display: block; } \ No newline at end of file diff --git a/frontend/ts/author-messages-element.ts b/frontend/ts/author-messages-element.ts deleted file mode 100644 index d9c0081..0000000 --- a/frontend/ts/author-messages-element.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Author, Message } from "./message" -import util from "./util" -import { BatchTimer } from "./batch-timer" -import { AdapterState } from "./adapter" - -export class AuthorMessagesElement extends HTMLElement { - static observedAttributes = [ "data-latest", "data-adapter", "data-target" ]; - - private _id: string | null = null; - private _adapter: string = ""; - - private _interactable: boolean = false; - - private _messages: Message[] = []; - private _byAuthorTimer: BatchTimer | null = null; - - constructor() { - super(); - this.innerHTML = ``; - } - - connectedCallback() { - this._id = this.getAttribute("data-target"); - this._adapter = this.getAttribute("data-adapter") ?? ""; - const gateway = this.getAttribute("data-gateway") ?? ""; - this._byAuthorTimer = new BatchTimer(gateway, this._adapter, "byAuthor"); - this._interactable = this.getAttribute("data-interactable") != null; - } - - attributeChangedCallback(attr: string, prev: string, next: string) { - switch (attr) { - case "data-target": - if (!next) { - return - } - this._id = next; - if (this._byAuthorTimer) { - this._byAuthorTimer.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; - } - let msg = datastore.messages.get(next); - if (msg) { - const existingIdx = this._messages.findIndex(m=>(m.renoteId ?? m.id) == (msg.renoteId ?? msg.id) && ((m.edited ?? m.created) <= (msg.edited ?? msg.created))); - - // first we update the backing data store - if (existingIdx >= 0) { - this._messages[existingIdx] = msg; - } else if (!this._messages.some(m=>(m.renoteId ?? m.id) == (msg.renoteId ?? msg.id))) { - this._messages.push(msg); - } - - - const ul = this.children[0]; - if (ul) { - // first pass through the dom, try to update a message if it's there - for (let i = 0; i < ul.childElementCount; i++){ - const id = ul.children[i]?.children[0]?.getAttribute("data-target"); - const ogMsg = this._messages.find(m=>(m.renoteId ?? m.id) == id); - if (ogMsg && existingIdx >= 0) { - ul.children[i]?.children[0]?.setAttribute("data-latest", id ?? ""); - return; - } - } - - // if we made it this far, let's create a node - const e = document.createElement("li"); - e.innerHTML = `` - // second pass, try to place it in reverse-chronological order - for (let i = 0; i < ul.childElementCount; i++){ - const id = ul.children[i]?.children[0]?.getAttribute("data-target"); - const ogMsg = this._messages.find(m=>(m.renoteId ?? m.id) == id); - if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) <= (msg.renoteTime ?? msg.created)) { - ul.insertBefore(e, ul.children[i]) - e.children[0].setAttribute("data-latest", next); - return; - } - } - - // final pass, we must be the earliest child (or maybe the first one to be rendered) - ul.append(e); - e.children[0].setAttribute("data-latest", next); - } - } - } - } -} \ No newline at end of file diff --git a/frontend/ts/batch-timer.ts b/frontend/ts/fetcher.ts similarity index 97% rename from frontend/ts/batch-timer.ts rename to frontend/ts/fetcher.ts index f11158b..15fb576 100644 --- a/frontend/ts/batch-timer.ts +++ b/frontend/ts/fetcher.ts @@ -1,6 +1,6 @@ import util from './util' -export class BatchTimer { +export class Fetcher { private _batch: string[]; private _timer: number; private _reqFn: (id: string[])=>void; diff --git a/frontend/ts/message-element.ts b/frontend/ts/message-element.ts index cc5fe6d..dd679ed 100644 --- a/frontend/ts/message-element.ts +++ b/frontend/ts/message-element.ts @@ -1,6 +1,6 @@ import util from "./util" import { Message } from "./message" -import { BatchTimer } from "./batch-timer" +import { Fetcher } from "./fetcher" import { AdapterState } from "./adapter" export class MessageElement extends HTMLElement { @@ -12,8 +12,9 @@ export class MessageElement extends HTMLElement { private _message: Message | null = null; private _interactable: boolean = false; private _replyWith: string | null = null; + private _inspectWith: string | null = null; - private _messageTimer: BatchTimer | null = null; + private _msgFetcher: Fetcher | null = null; constructor() { super(); @@ -24,9 +25,10 @@ export class MessageElement extends HTMLElement { this._id = this.getAttribute("data-target"); this._adapter = this.getAttribute("data-adapter"); const gateway = this.getAttribute("data-gateway") ?? ""; - this._messageTimer = new BatchTimer(gateway, this._adapter ?? "", "message"); + this._msgFetcher = new Fetcher(gateway, this._adapter ?? "", "message"); this._interactable = this.getAttribute("data-interactable") != null; this._replyWith = this.getAttribute("data-replywith"); + this._inspectWith = this.getAttribute("data-inspectwith"); } attributeChangedCallback(attr: string, prev: string, next: string) { @@ -38,8 +40,8 @@ export class MessageElement extends HTMLElement { this._id = next; this._message = null; this.innerHTML = `
`; - if (this._messageTimer) { - this._messageTimer.queue(next, 100); + if (this._msgFetcher) { + this._msgFetcher.queue(next, 100); } break; case "data-latest": @@ -49,7 +51,6 @@ export class MessageElement extends HTMLElement { return; } let msg = datastore.messages.get(next); - console.log("MessageElement.attributeChangedCallback: " + JSON.stringify(msg)); if (msg) { this._message = msg; const metadata = this.querySelector(".message_metadata"); @@ -58,11 +59,29 @@ export class MessageElement extends HTMLElement { const interactions = this.querySelector(".message_interactions"); if (metadata) { if (this._message.renoteId) { - metadata.innerHTML = `${this._message.renoter}${new Date(this._message.renoteTime ?? 0)}` + metadata.innerHTML = `${this._message.renoter} boosted${new Date(this._message.renoteTime ?? 0)}` } else { metadata.innerHTML = ""; } - metadata.innerHTML += `${this._message.author}${new Date(this._message.created)}${this._message.visibility}${this._message.protocol}` + metadata.innerHTML += `${this._message.author}${new Date(this._message.created)}${this._message.uri}${this._message.replyTo ? "reply to " + this._message.replyTo + "" : ""}${this._message.visibility}${this._message.protocol}` + + const renoter = this.querySelector(".message_renoter"); + const author = this.querySelector(".message_author"); + const url = this.querySelector(".message_url"); + const replyToUrl = this.querySelector(".message_inreplyto"); + + if (renoter) { + renoter.addEventListener("click", this.inspect(this._message.renoter ?? "", "author")); + } + if (author) { + author.addEventListener("click", this.inspect(this._message.author, "author")); + } + if (url) { + url.addEventListener("click", this.inspect(this._message.uri, "message")); + } + if (replyToUrl) { + replyToUrl.addEventListener("click", this.inspect(this._message.replyTo ?? "", "message")); + } } if (content) { content.innerHTML = this._message.content; @@ -110,7 +129,7 @@ export class MessageElement extends HTMLElement { attachments.innerHTML = html; } if (this._interactable && interactions) { - interactions.innerHTML = `permalink` + interactions.innerHTML = `` const replyBtn = this.querySelector(".message_reply"); const boostBtn = this.querySelector(".message_boost"); if (replyBtn) { @@ -139,4 +158,17 @@ export class MessageElement extends HTMLElement { private boost() { // use a Doer to boost } + + private inspect(target: string, type: string): ()=>void { + const self = this; + + return ()=> { + const e = document.querySelector(`#${self._inspectWith}`); + if (e) { + e.setAttribute("data-" + type, target); + } else { + window.open(target, "_blank"); + } + } + } } \ No newline at end of file diff --git a/frontend/ts/navigator.ts b/frontend/ts/navigator.ts new file mode 100644 index 0000000..c4132eb --- /dev/null +++ b/frontend/ts/navigator.ts @@ -0,0 +1,108 @@ +import { AdapterState } from "./adapter" + +class HistoryNode { + id: string; + type: string; + prev: HistoryNode | null = null; + next: HistoryNode | null = null; + + constructor(id: string, type: string) { + this.id = id; + this.type = type; + } +} + +export class NavigatorElement extends HTMLElement { + static observedAttributes = [ "data-author", "data-message" ]; + + private _adapter: string = ""; + private _history: HistoryNode | null = null; + private _replyWith: string | null = null; + private _gateway: string = ""; + constructor() { + super(); + this.innerHTML = `` + } + + connectedCallback() { + this._adapter = this.getAttribute("data-adapter") ?? ""; + this._replyWith = this.getAttribute("data-replywith"); + this._gateway = this.getAttribute("data-gateway") ?? ""; + + const prevBtn = this.querySelector(".nav_prev"); + const nextBtn = this.querySelector(".nav_next"); + const clearBtn = this.querySelector(".nav_clear"); + + if (prevBtn) { + prevBtn.addEventListener("click", this.goPrev); + } + if (nextBtn) { + nextBtn.addEventListener("click", this.goNext); + } + if (clearBtn) { + clearBtn.addEventListener("click", this.clear); + } + } + + attributeChangedCallback(attr: string, prev: string, next: string) { + switch (attr) { + case "data-author": + case "data-message": + if (next == prev) { + return; + } + if (this._history && this._history.prev && this._history.prev.id == next) { + this._history = this._history.prev; + } else { + const h = this._history; + this._history = new HistoryNode(next, attr.slice(attr.indexOf("-") + 1)); + this._history.prev = h; + } + const panel = this.querySelector(".nav_container"); + const datastore = AdapterState._instance.data.get(this._adapter); + if (datastore && panel) { + switch (attr) { + case "data-author": + const author = datastore.profileCache.get(next); + panel.innerHTML = `` + + const profile = this.querySelector("underbbs-profile"); + const tl = this.querySelector("underbbs-timeline"); + if (profile && tl) { + if (!author) { + profile.setAttribute("data-target", next); + } else { + profile.setAttribute("data-latest", next); + } + tl.setAttribute("data-target", next); + } + break; + case "data-message": + const msg = datastore.messages.get(next); + panel.innerHTML = `` + const e = this.querySelector("underbbs-message"); + if (e) { + if (!msg) { + e.setAttribute("data-target", next); + } else { + e.setAttribute("data-latest", next); + } + } + break; + + } + + } + + } + } + + private goNext() { + } + + private goPrev() { + } + + private clear() { + } +} \ No newline at end of file diff --git a/frontend/ts/profile-element.ts b/frontend/ts/profile-element.ts index 4ffd2df..e5bbe9a 100644 --- a/frontend/ts/profile-element.ts +++ b/frontend/ts/profile-element.ts @@ -1,6 +1,6 @@ import { Author } from "./message" import util from "./util" -import { BatchTimer } from "./batch-timer" +import { Fetcher } from "./fetcher" import { AdapterState } from "./adapter" export class ProfileElement extends HTMLElement { @@ -11,7 +11,7 @@ export class ProfileElement extends HTMLElement { private _author: Author | null = null; - private _authorTimer: BatchTimer | null = null; + private _authorFetcher: Fetcher | null = null; constructor() { @@ -24,7 +24,7 @@ export class ProfileElement extends HTMLElement { this._id = this.getAttribute("data-target"); this._adapter = this.getAttribute("data-adapter") ?? ""; const gateway = this.getAttribute("data-gateway") ?? ""; - this._authorTimer = new BatchTimer(gateway, this._adapter, "author"); + this._authorFetcher = new Fetcher(gateway, this._adapter, "author"); } attributeChangedCallback(attr: string, prev: string, next: string) { @@ -35,8 +35,8 @@ export class ProfileElement extends HTMLElement { return } this._id = next; - if (this._authorTimer) { - this._authorTimer.queue(next, 100); + if (this._authorFetcher) { + this._authorFetcher.queue(next, 100); } break; case "data-latest": diff --git a/frontend/ts/timeline-element.ts b/frontend/ts/timeline-element.ts index 472b1fb..b635e1f 100644 --- a/frontend/ts/timeline-element.ts +++ b/frontend/ts/timeline-element.ts @@ -1,6 +1,7 @@ import { Author, Message } from "./message" import util from "./util" import { Subscriber } from "./subscriber" +import { Fetcher } from "./fetcher" import { AdapterState } from "./adapter" export class TimelineElement extends HTMLElement { @@ -11,10 +12,13 @@ export class TimelineElement extends HTMLElement { private _interactable: boolean = false; private _replyWith: string | null = null; + private _inspectWith: string | null = null; + private _mode: string = "subscribe"; private _messages: Message[] = []; private _subscriber: Subscriber | null = null; - + private _byAuthorFetcher: Fetcher | null = null; + constructor() { super(); this.innerHTML = ``; @@ -25,8 +29,11 @@ export class TimelineElement extends HTMLElement { this._adapter = this.getAttribute("data-adapter") ?? ""; const gateway = this.getAttribute("data-gateway") ?? ""; this._subscriber = new Subscriber(gateway, this._adapter ?? "", this.getAttribute("id") ?? null); + this._byAuthorFetcher = new Fetcher(gateway, this._adapter, "byAuthor"); this._interactable = this.getAttribute("data-interactable") != null; this._replyWith = this.getAttribute("data-replywith"); + this._inspectWith = this.getAttribute("data-inspectwith"); + this._mode = this.getAttribute("data-mode") ?? "subscribe"; } attributeChangedCallback(attr: string, prev: string, next: string) { @@ -38,8 +45,17 @@ export class TimelineElement extends HTMLElement { this._timeline = next; this.innerHTML = ``; this._messages = []; - if (this._subscriber) { - this._subscriber.subscribe(next); + switch (this._mode) { + case "byAuthor": + if (this._byAuthorFetcher) { + this._byAuthorFetcher.queue(next, 100); + } + break; + case "subscribe": + if (this._subscriber) { + this._subscriber.subscribe(next); + } + break; } break; case "data-latest": @@ -75,7 +91,7 @@ export class TimelineElement extends HTMLElement { // if we made it this far, let's create a node const e = document.createElement("li"); - e.innerHTML = `` + e.innerHTML = `` // second pass, try to place it in reverse-chronological order for (let i = 0; i < ul.childElementCount; i++){ const id = ul.children[i]?.children[0]?.getAttribute("data-target"); diff --git a/frontend/ts/util.ts b/frontend/ts/util.ts index a0f417e..365a9d5 100644 --- a/frontend/ts/util.ts +++ b/frontend/ts/util.ts @@ -1,5 +1,4 @@ import { DatagramSocket } from './websocket' -import { BatchTimer } from './batch-timer' function _(key: string, value: any | null | undefined = undefined): any | null { const x = window; diff --git a/frontend/ts/websocket.ts b/frontend/ts/websocket.ts index bff8e2f..0ff1ced 100644 --- a/frontend/ts/websocket.ts +++ b/frontend/ts/websocket.ts @@ -3,12 +3,12 @@ import {AdapterState, AdapterData} from "./adapter"; import {Message, Attachment, Author} from "./message" import {Settings} from "./settings" import {SettingsElement} from "./settings-element" -import {AuthorMessagesElement} from "./author-messages-element" import {ProfileElement} from "./profile-element" import {MessageElement} from "./message-element" import {TimelineElement} from "./timeline-element" import {TimelineFilterElement} from "./timeline-filter-element" import {CreateMessageElement} from "./create-message-element" +import {NavigatorElement} from "./navigator" export class DatagramSocket { public static skey: string | null = null; @@ -39,11 +39,6 @@ export class DatagramSocket { const target = p.getAttribute("data-target"); p.setAttribute("data-target", target ?? ""); }); - const feeds = document.querySelectorAll("underbbs-author-messages"); - feeds.forEach(f=>{ - const target = f.getAttribute("data-target"); - f.setAttribute("data-target", target ?? ""); - }); const timelines = document.querySelectorAll("underbbs-timeline"); timelines.forEach(t=>{ const target = t.getAttribute("data-target"); @@ -130,10 +125,10 @@ function init() { customElements.define("underbbs-message", MessageElement); customElements.define("underbbs-settings", SettingsElement); customElements.define("underbbs-profile", ProfileElement); - customElements.define("underbbs-author-messages", AuthorMessagesElement); customElements.define("underbbs-timeline", TimelineElement); customElements.define("underbbs-timeline-filter", TimelineFilterElement); customElements.define("underbbs-create-message", CreateMessageElement); + customElements.define("underbbs-navigator", NavigatorElement); console.log("underbbs initialized!") }