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]}) == "@" {
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

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;
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;
}

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'
export class BatchTimer {
export class Fetcher {
private _batch: string[];
private _timer: number;
private _reqFn: (id: string[])=>void;

View file

@ -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 = `<div class="message_metadata"></div><div class="message_content"></div><div class="message_attachments"></div><div class="message_interactions"></div>`;
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 = `<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 {
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) {
content.innerHTML = this._message.content;
@ -110,7 +129,7 @@ export class MessageElement extends HTMLElement {
attachments.innerHTML = html;
}
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 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");
}
}
}
}

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 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":

View file

@ -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,9 +12,12 @@ 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();
@ -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 = `<ul class="messages_list"></ul>`;
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 = `<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
for (let i = 0; i < ul.childElementCount; i++){
const id = ul.children[i]?.children[0]?.getAttribute("data-target");

View file

@ -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 = <any>window;

View file

@ -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!")
}