merge author-messages and timeline components, start implementing navigator for viewing individual messages and authors

This commit is contained in:
Iris Lightshard 2024-12-11 20:19:11 -07:00
parent 2ff69b9d38
commit 1806140f4b
Signed by: Iris Lightshard
GPG key ID: 688407174966CAF3
10 changed files with 207 additions and 156 deletions

View file

@ -225,11 +225,27 @@ func (self *anonAPAdapter) Fetch(etype string, ids []string) error {
if string([]byte{id[0]}) == "@" { if string([]byte{id[0]}) == "@" {
id = id[1:] 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 { if err != nil {
return err return err
} }
fmt.Printf("%d\n", res.StatusCode)
data := getBodyJson(res) data := getBodyJson(res)
fmt.Println(string(data))
wf := webFinger{} wf := webFinger{}
json.Unmarshal(data, &wf) json.Unmarshal(data, &wf)
var profile string var profile string

View file

@ -1,11 +1,3 @@
:root {
--bg_color: #000000;
--fg_color: #ccc;
--main_color: #1f9b92;
--sub_color: #002b36;
--err_color: #DC143C;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
@ -17,17 +9,6 @@
background: var(--bg_color); 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 { a {
color: var(--main_color); color: var(--main_color);
} }
@ -44,27 +25,23 @@ input {
color: var(--err_color); color: var(--err_color);
} }
nav ul li { underbbs-message, underbbs-profile {
display: inline; max-width: 70ch;
padding: 0.5em; display: block;
}
underbbs-message img {
max-width: 100%;
}
underbbs-profile img {
max-width: 200px;
} }
nav { nav {
padding: 1em; padding: 1em;
} }
nav ul li a { underbbs-message .message_metadata span {
text-decoration: none; display: block;
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;
} }

View file

@ -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 = `<ul class="messages_list"></ul>`;
}
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 = `<underbbs-message data-adapter="${this._adapter}" data-target="${next}" ${this._interactable ? "data-interactable" : ""}></underbbs-message>`
// 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);
}
}
}
}
}

View file

@ -1,6 +1,6 @@
import util from './util' import util from './util'
export class BatchTimer { export class Fetcher {
private _batch: string[]; private _batch: string[];
private _timer: number; private _timer: number;
private _reqFn: (id: string[])=>void; private _reqFn: (id: string[])=>void;

View file

@ -1,6 +1,6 @@
import util from "./util" import util from "./util"
import { Message } from "./message" import { Message } from "./message"
import { BatchTimer } from "./batch-timer" import { Fetcher } from "./fetcher"
import { AdapterState } from "./adapter" import { AdapterState } from "./adapter"
export class MessageElement extends HTMLElement { export class MessageElement extends HTMLElement {
@ -12,8 +12,9 @@ export class MessageElement extends HTMLElement {
private _message: Message | null = null; private _message: Message | null = null;
private _interactable: boolean = false; private _interactable: boolean = false;
private _replyWith: string | null = null; private _replyWith: string | null = null;
private _inspectWith: string | null = null;
private _messageTimer: BatchTimer | null = null; private _msgFetcher: Fetcher | null = null;
constructor() { constructor() {
super(); super();
@ -24,9 +25,10 @@ export class MessageElement extends HTMLElement {
this._id = this.getAttribute("data-target"); this._id = this.getAttribute("data-target");
this._adapter = this.getAttribute("data-adapter"); this._adapter = this.getAttribute("data-adapter");
const gateway = this.getAttribute("data-gateway") ?? ""; 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._interactable = this.getAttribute("data-interactable") != null;
this._replyWith = this.getAttribute("data-replywith"); this._replyWith = this.getAttribute("data-replywith");
this._inspectWith = this.getAttribute("data-inspectwith");
} }
attributeChangedCallback(attr: string, prev: string, next: string) { attributeChangedCallback(attr: string, prev: string, next: string) {
@ -38,8 +40,8 @@ export class MessageElement extends HTMLElement {
this._id = next; this._id = next;
this._message = null; this._message = null;
this.innerHTML = `<div class="message_metadata"></div><div class="message_content"></div><div class="message_attachments"></div><div class="message_interactions"></div>`; this.innerHTML = `<div class="message_metadata"></div><div class="message_content"></div><div class="message_attachments"></div><div class="message_interactions"></div>`;
if (this._messageTimer) { if (this._msgFetcher) {
this._messageTimer.queue(next, 100); this._msgFetcher.queue(next, 100);
} }
break; break;
case "data-latest": case "data-latest":
@ -49,7 +51,6 @@ export class MessageElement extends HTMLElement {
return; return;
} }
let msg = datastore.messages.get(next); let msg = datastore.messages.get(next);
console.log("MessageElement.attributeChangedCallback: " + JSON.stringify(msg));
if (msg) { if (msg) {
this._message = msg; this._message = msg;
const metadata = this.querySelector(".message_metadata"); const metadata = this.querySelector(".message_metadata");
@ -58,11 +59,29 @@ export class MessageElement extends HTMLElement {
const interactions = this.querySelector(".message_interactions"); const interactions = this.querySelector(".message_interactions");
if (metadata) { if (metadata) {
if (this._message.renoteId) { if (this._message.renoteId) {
metadata.innerHTML = `<span class="message_renoter">${this._message.renoter}</span><span class="message_renotetime">${new Date(this._message.renoteTime ?? 0)}</span>` metadata.innerHTML = `<span class="message_renoter">${this._message.renoter} boosted</span><span class="message_renotetime">${new Date(this._message.renoteTime ?? 0)}</span>`
} else { } else {
metadata.innerHTML = ""; metadata.innerHTML = "";
} }
metadata.innerHTML += `<span class="message_author">${this._message.author}</span><span class="message_timestamp">${new Date(this._message.created)}</span><span class="message_visibility">${this._message.visibility}</span><span class="message_protocol">${this._message.protocol}</span>` metadata.innerHTML += `<span class="message_author">${this._message.author}</span><span class="message_timestamp">${new Date(this._message.created)}</span><span class="message_url">${this._message.uri}</span>${this._message.replyTo ? "<span class='message_inreplyto'>reply to " + this._message.replyTo + "</span>" : ""}<span class="message_visibility">${this._message.visibility}</span><span class="message_protocol">${this._message.protocol}</span>`
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) { if (content) {
content.innerHTML = this._message.content; content.innerHTML = this._message.content;
@ -110,7 +129,7 @@ export class MessageElement extends HTMLElement {
attachments.innerHTML = html; attachments.innerHTML = html;
} }
if (this._interactable && interactions) { if (this._interactable && interactions) {
interactions.innerHTML = `<button class="message_reply">reply</button><button class="message_boost">boost</button><a target="_blank" href="${this._message.uri}">permalink</a>` interactions.innerHTML = `<button class="message_reply">reply</button><button class="message_boost">boost</button>`
const replyBtn = this.querySelector(".message_reply"); const replyBtn = this.querySelector(".message_reply");
const boostBtn = this.querySelector(".message_boost"); const boostBtn = this.querySelector(".message_boost");
if (replyBtn) { if (replyBtn) {
@ -139,4 +158,17 @@ export class MessageElement extends HTMLElement {
private boost() { private boost() {
// use a Doer to 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");
}
}
}
} }

108
frontend/ts/navigator.ts Normal file
View file

@ -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 = `<nav><button class="nav_prev">&larr;</button><button class="nav_next">&rarr;</button><button class="nav_clear">&times;</button></nav><div class="nav_container"></div>`
}
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 = `<underbbs-profile data-gateway="${this._gateway}" data-adapter="${this._adapter}" data-target="${next}"></underbbs-profile><underbbs-timeline data-gateway="${this._gateway}" data-target="${next}" data-adapter="${this._adapter}" data-interactable data-mode="fetch" data-inspectwith="${this.getAttribute("id")??""}" data-replywith="${this._replyWith}"></underbbs-timeline>`
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 = `<underbbs-message data-gateway="${this._gateway}" data-adapter="${this._adapter}" data-target="${next}" data-interactable data-inspectwith="${this.getAttribute("id")??""}" data-replywith="${this._replyWith}"></underbbs-message>`
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() {
}
}

View file

@ -1,6 +1,6 @@
import { Author } from "./message" import { Author } from "./message"
import util from "./util" import util from "./util"
import { BatchTimer } from "./batch-timer" import { Fetcher } from "./fetcher"
import { AdapterState } from "./adapter" import { AdapterState } from "./adapter"
export class ProfileElement extends HTMLElement { export class ProfileElement extends HTMLElement {
@ -11,7 +11,7 @@ export class ProfileElement extends HTMLElement {
private _author: Author | null = null; private _author: Author | null = null;
private _authorTimer: BatchTimer | null = null; private _authorFetcher: Fetcher | null = null;
constructor() { constructor() {
@ -24,7 +24,7 @@ export class ProfileElement extends HTMLElement {
this._id = this.getAttribute("data-target"); this._id = this.getAttribute("data-target");
this._adapter = this.getAttribute("data-adapter") ?? ""; this._adapter = this.getAttribute("data-adapter") ?? "";
const gateway = this.getAttribute("data-gateway") ?? ""; 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) { attributeChangedCallback(attr: string, prev: string, next: string) {
@ -35,8 +35,8 @@ export class ProfileElement extends HTMLElement {
return return
} }
this._id = next; this._id = next;
if (this._authorTimer) { if (this._authorFetcher) {
this._authorTimer.queue(next, 100); this._authorFetcher.queue(next, 100);
} }
break; break;
case "data-latest": case "data-latest":

View file

@ -1,6 +1,7 @@
import { Author, Message } from "./message" import { Author, Message } from "./message"
import util from "./util" import util from "./util"
import { Subscriber } from "./subscriber" import { Subscriber } from "./subscriber"
import { Fetcher } from "./fetcher"
import { AdapterState } from "./adapter" import { AdapterState } from "./adapter"
export class TimelineElement extends HTMLElement { export class TimelineElement extends HTMLElement {
@ -11,9 +12,12 @@ export class TimelineElement extends HTMLElement {
private _interactable: boolean = false; private _interactable: boolean = false;
private _replyWith: string | null = null; private _replyWith: string | null = null;
private _inspectWith: string | null = null;
private _mode: string = "subscribe";
private _messages: Message[] = []; private _messages: Message[] = [];
private _subscriber: Subscriber | null = null; private _subscriber: Subscriber | null = null;
private _byAuthorFetcher: Fetcher | null = null;
constructor() { constructor() {
super(); super();
@ -25,8 +29,11 @@ export class TimelineElement extends HTMLElement {
this._adapter = this.getAttribute("data-adapter") ?? ""; this._adapter = this.getAttribute("data-adapter") ?? "";
const gateway = this.getAttribute("data-gateway") ?? ""; const gateway = this.getAttribute("data-gateway") ?? "";
this._subscriber = new Subscriber(gateway, this._adapter ?? "", this.getAttribute("id") ?? null); 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._interactable = this.getAttribute("data-interactable") != null;
this._replyWith = this.getAttribute("data-replywith"); 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) { attributeChangedCallback(attr: string, prev: string, next: string) {
@ -38,10 +45,19 @@ export class TimelineElement extends HTMLElement {
this._timeline = next; this._timeline = next;
this.innerHTML = `<ul class="messages_list"></ul>`; this.innerHTML = `<ul class="messages_list"></ul>`;
this._messages = []; this._messages = [];
switch (this._mode) {
case "byAuthor":
if (this._byAuthorFetcher) {
this._byAuthorFetcher.queue(next, 100);
}
break;
case "subscribe":
if (this._subscriber) { if (this._subscriber) {
this._subscriber.subscribe(next); this._subscriber.subscribe(next);
} }
break; break;
}
break;
case "data-latest": case "data-latest":
let datastore = AdapterState._instance.data.get(this._adapter); let datastore = AdapterState._instance.data.get(this._adapter);
if (!datastore) { if (!datastore) {
@ -75,7 +91,7 @@ export class TimelineElement extends HTMLElement {
// if we made it this far, let's create a node // if we made it this far, let's create a node
const e = document.createElement("li"); const e = document.createElement("li");
e.innerHTML = `<underbbs-message data-adapter="${this._adapter}" data-target="${next}" ${this._interactable ? "data-interactable" : ""} ${this._replyWith ? "data-replywith='" + this._replyWith + "'" : ""}></underbbs-message>` e.innerHTML = `<underbbs-message data-adapter="${this._adapter}" data-target="${next}" ${this._interactable ? "data-interactable" : ""} ${this._replyWith ? "data-replywith='" + this._replyWith + "'" : ""} ${this._inspectWith ? "data-inspectwith='" + this._inspectWith + "'": ""}></underbbs-message>`
// second pass, try to place it in reverse-chronological order // second pass, try to place it in reverse-chronological order
for (let i = 0; i < ul.childElementCount; i++){ for (let i = 0; i < ul.childElementCount; i++){
const id = ul.children[i]?.children[0]?.getAttribute("data-target"); const id = ul.children[i]?.children[0]?.getAttribute("data-target");

View file

@ -1,5 +1,4 @@
import { DatagramSocket } from './websocket' import { DatagramSocket } from './websocket'
import { BatchTimer } from './batch-timer'
function _(key: string, value: any | null | undefined = undefined): any | null { function _(key: string, value: any | null | undefined = undefined): any | null {
const x = <any>window; const x = <any>window;

View file

@ -3,12 +3,12 @@ import {AdapterState, AdapterData} from "./adapter";
import {Message, Attachment, Author} from "./message" import {Message, Attachment, Author} from "./message"
import {Settings} from "./settings" import {Settings} from "./settings"
import {SettingsElement} from "./settings-element" import {SettingsElement} from "./settings-element"
import {AuthorMessagesElement} from "./author-messages-element"
import {ProfileElement} from "./profile-element" import {ProfileElement} from "./profile-element"
import {MessageElement} from "./message-element" import {MessageElement} from "./message-element"
import {TimelineElement} from "./timeline-element" import {TimelineElement} from "./timeline-element"
import {TimelineFilterElement} from "./timeline-filter-element" import {TimelineFilterElement} from "./timeline-filter-element"
import {CreateMessageElement} from "./create-message-element" import {CreateMessageElement} from "./create-message-element"
import {NavigatorElement} from "./navigator"
export class DatagramSocket { export class DatagramSocket {
public static skey: string | null = null; public static skey: string | null = null;
@ -39,11 +39,6 @@ export class DatagramSocket {
const target = p.getAttribute("data-target"); const target = p.getAttribute("data-target");
p.setAttribute("data-target", 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"); const timelines = document.querySelectorAll("underbbs-timeline");
timelines.forEach(t=>{ timelines.forEach(t=>{
const target = t.getAttribute("data-target"); const target = t.getAttribute("data-target");
@ -130,10 +125,10 @@ function init() {
customElements.define("underbbs-message", MessageElement); customElements.define("underbbs-message", MessageElement);
customElements.define("underbbs-settings", SettingsElement); customElements.define("underbbs-settings", SettingsElement);
customElements.define("underbbs-profile", ProfileElement); customElements.define("underbbs-profile", ProfileElement);
customElements.define("underbbs-author-messages", AuthorMessagesElement);
customElements.define("underbbs-timeline", TimelineElement); customElements.define("underbbs-timeline", TimelineElement);
customElements.define("underbbs-timeline-filter", TimelineFilterElement); customElements.define("underbbs-timeline-filter", TimelineFilterElement);
customElements.define("underbbs-create-message", CreateMessageElement); customElements.define("underbbs-create-message", CreateMessageElement);
customElements.define("underbbs-navigator", NavigatorElement);
console.log("underbbs initialized!") console.log("underbbs initialized!")
} }