remove old components, get subscribe/timeline working with honk adapter
This commit is contained in:
parent
b00f26b17d
commit
d61c786ffd
23 changed files with 235 additions and 570 deletions
|
@ -7,7 +7,7 @@ import (
|
||||||
type Adapter interface {
|
type Adapter interface {
|
||||||
Init(Settings, *chan SocketData) error
|
Init(Settings, *chan SocketData) error
|
||||||
Name() string
|
Name() string
|
||||||
Subscribe(string) []error
|
Subscribe(string, *string) []error
|
||||||
Fetch(string, []string) error
|
Fetch(string, []string) error
|
||||||
Do(string, map[string]string) error
|
Do(string, map[string]string) error
|
||||||
DefaultSubscriptionFilter() string
|
DefaultSubscriptionFilter() string
|
||||||
|
|
|
@ -137,10 +137,11 @@ 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 = 9999999
|
||||||
}
|
}
|
||||||
jsonData := make([]byte, l)
|
jsonData := make([]byte, l)
|
||||||
res.Body.Read(jsonData)
|
res.Body.Read(jsonData)
|
||||||
|
|
||||||
return jsonData
|
return jsonData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,8 @@ type donk struct {
|
||||||
type honk struct {
|
type honk struct {
|
||||||
ID int
|
ID int
|
||||||
Honker string
|
Honker string
|
||||||
Handles []string
|
Handles string
|
||||||
Oonker *string
|
Oonker string
|
||||||
XID string
|
XID string
|
||||||
RID *string
|
RID *string
|
||||||
Noise string
|
Noise string
|
||||||
|
@ -33,6 +33,12 @@ type honk struct {
|
||||||
Date string
|
Date string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type honkResponse struct {
|
||||||
|
Honks []honk `json:"honks"`
|
||||||
|
ChatCount int `json:"chatcount"`
|
||||||
|
MeCount int `json:"mecount"`
|
||||||
|
}
|
||||||
|
|
||||||
type HonkAdapter struct {
|
type HonkAdapter struct {
|
||||||
data *chan SocketData
|
data *chan SocketData
|
||||||
nickname string
|
nickname string
|
||||||
|
@ -98,19 +104,19 @@ func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *HonkAdapter) Subscribe(filter string) []error {
|
func (self *HonkAdapter) Subscribe(filter string, target *string) []error {
|
||||||
if self.stop != nil {
|
if self.stop != nil {
|
||||||
close(self.stop)
|
close(self.stop)
|
||||||
self.maxId = 0
|
self.maxId = 0
|
||||||
}
|
}
|
||||||
self.stop = make(chan bool)
|
self.stop = make(chan bool)
|
||||||
|
|
||||||
go self.gethonks(filter)
|
go self.gethonks(filter, target)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *HonkAdapter) gethonks(filter string) {
|
func (self *HonkAdapter) gethonks(filter string, target *string) {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -119,6 +125,7 @@ func (self *HonkAdapter) gethonks(filter string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
fmt.Println("bout to get honks")
|
||||||
honkForm := url.Values{
|
honkForm := url.Values{
|
||||||
"action": []string{"gethonks"},
|
"action": []string{"gethonks"},
|
||||||
"token": []string{self.token},
|
"token": []string{self.token},
|
||||||
|
@ -129,24 +136,32 @@ func (self *HonkAdapter) gethonks(filter string) {
|
||||||
}
|
}
|
||||||
res, err := http.PostForm(self.server+"/api", honkForm)
|
res, err := http.PostForm(self.server+"/api", honkForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// return?
|
fmt.Println("fucked up: " + err.Error())
|
||||||
|
self.stop <- true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
honksData := getBodyJson(res)
|
fmt.Println("we got some honks")
|
||||||
honks := []honk{}
|
hr := honkResponse{}
|
||||||
json.Unmarshal(honksData, &honks)
|
err = json.NewDecoder(res.Body).Decode(&hr)
|
||||||
for _, h := range honks {
|
if err != nil {
|
||||||
|
fmt.Println("malformed honks: " + err.Error())
|
||||||
|
self.stop <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, h := range hr.Honks {
|
||||||
if h.ID > self.maxId {
|
if h.ID > self.maxId {
|
||||||
self.maxId = h.ID
|
self.maxId = h.ID
|
||||||
msg := self.toMsg(h)
|
|
||||||
self.send(msg)
|
|
||||||
}
|
}
|
||||||
|
msg := self.toMsg(h, target)
|
||||||
|
fmt.Println("gonna send a honk on the channel dawg")
|
||||||
|
self.send(msg)
|
||||||
}
|
}
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *HonkAdapter) toMsg(h honk) Message {
|
func (self *HonkAdapter) toMsg(h honk, target *string) Message {
|
||||||
t, err := time.Parse(time.RFC3339, h.Date)
|
t, err := time.Parse(time.RFC3339, h.Date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t = time.Now()
|
t = time.Now()
|
||||||
|
@ -154,8 +169,8 @@ func (self *HonkAdapter) toMsg(h honk) Message {
|
||||||
tt := t.UnixMilli()
|
tt := t.UnixMilli()
|
||||||
|
|
||||||
a := h.Honker
|
a := h.Honker
|
||||||
if h.Oonker != nil {
|
if h.Oonker != "" {
|
||||||
a = *h.Oonker
|
a = h.Oonker
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := Message{
|
msg := Message{
|
||||||
|
@ -175,9 +190,9 @@ func (self *HonkAdapter) toMsg(h honk) Message {
|
||||||
if h.Public {
|
if h.Public {
|
||||||
msg.Visibility = "Public"
|
msg.Visibility = "Public"
|
||||||
}
|
}
|
||||||
if h.Oonker != nil {
|
if h.Oonker != "" {
|
||||||
r := fmt.Sprintf("%s/bonk/%d", h.Honker, h.ID)
|
r := fmt.Sprintf("%s/bonk/%d", h.Honker, h.ID)
|
||||||
msg.Renoter = h.Oonker
|
msg.Renoter = &h.Oonker
|
||||||
msg.RenoteId = &r
|
msg.RenoteId = &r
|
||||||
msg.RenoteTime = &tt
|
msg.RenoteTime = &tt
|
||||||
}
|
}
|
||||||
|
@ -188,6 +203,9 @@ func (self *HonkAdapter) toMsg(h honk) Message {
|
||||||
}
|
}
|
||||||
msg.Attachments = append(msg.Attachments, a)
|
msg.Attachments = append(msg.Attachments, a)
|
||||||
}
|
}
|
||||||
|
if target != nil {
|
||||||
|
msg.Target = target
|
||||||
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (self *MastoAdapter) Init(settings Settings, data *chan SocketData) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *MastoAdapter) Subscribe(filter string) []error {
|
func (self *MastoAdapter) Subscribe(filter string, target *string) []error {
|
||||||
// TODO: decode separate timelines and hashtags
|
// TODO: decode separate timelines and hashtags
|
||||||
// for now, the filter is just the timeline
|
// for now, the filter is just the timeline
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ func (self *MisskeyAdapter) Init(settings Settings, data *chan SocketData) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *MisskeyAdapter) Subscribe(filter string) []error {
|
func (self *MisskeyAdapter) Subscribe(filter string, target *string) []error {
|
||||||
// misskey streaming API is undocumented....
|
// misskey streaming API is undocumented....
|
||||||
// we could try to reverse engineer it by directly connecting to the websocket???
|
// we could try to reverse engineer it by directly connecting to the websocket???
|
||||||
// alternatively, we can poll timelines, mentions, etc with a cancellation channel,
|
// alternatively, we can poll timelines, mentions, etc with a cancellation channel,
|
||||||
|
|
|
@ -48,7 +48,7 @@ func (self *NostrAdapter) Init(settings Settings, data *chan SocketData) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *NostrAdapter) Subscribe(filter string) []error {
|
func (self *NostrAdapter) Subscribe(filter string, target *string) []error {
|
||||||
var filters nostr.Filters
|
var filters nostr.Filters
|
||||||
err := json.Unmarshal([]byte(filter), &filters)
|
err := json.Unmarshal([]byte(filter), &filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
5
frontend/dist/index.html
vendored
5
frontend/dist/index.html
vendored
|
@ -14,9 +14,14 @@
|
||||||
</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>
|
||||||
<main>
|
<main>
|
||||||
|
<div class='uicolumn'>
|
||||||
<details open><summary>settings</summary><div id="settings_parent"></div></details>
|
<details open><summary>settings</summary><div id="settings_parent"></div></details>
|
||||||
|
<details open><summary>timeline</summary><div id="timeline_parent"></div></details>
|
||||||
|
</div>
|
||||||
|
<div class='uicolumn'>
|
||||||
<details open><summary>profile</summary><div id="profile_parent"></div></details>
|
<details open><summary>profile</summary><div id="profile_parent"></div></details>
|
||||||
<details open><summary>honks</summary><div id="honks_parent"></div></details>
|
<details open><summary>honks</summary><div id="honks_parent"></div></details>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script src="./main.js" type="application/javascript"></script>
|
<script src="./main.js" type="application/javascript"></script>
|
||||||
|
|
3
frontend/dist/style.css
vendored
3
frontend/dist/style.css
vendored
|
@ -64,4 +64,7 @@ nav ul li a {
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
}
|
}
|
|
@ -1,285 +0,0 @@
|
||||||
import util from "./util"
|
|
||||||
|
|
||||||
import { Message, Author } from "./message"
|
|
||||||
import { MessageThread } from "./thread"
|
|
||||||
import { AdapterState } from "./adapter"
|
|
||||||
import { BatchTimer } from "./batch-timer"
|
|
||||||
|
|
||||||
export class AdapterElement extends HTMLElement {
|
|
||||||
static observedAttributes = [ "data-latest", "data-view", "data-viewing" ]
|
|
||||||
|
|
||||||
private _latest: string = "" ;
|
|
||||||
private _view: string = "";
|
|
||||||
private _name: string = ""
|
|
||||||
private _viewing: string = "";
|
|
||||||
|
|
||||||
private _convoyBatchTimer = new BatchTimer((ids: string[])=>{
|
|
||||||
let url = `/api/adapters/${this._name}/fetch?entity_type=convoy`;
|
|
||||||
for (let id of ids) {
|
|
||||||
url += `&entity_id=${id}`;
|
|
||||||
}
|
|
||||||
util.authorizedFetch("GET", url, null)
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: use visibility of the thread to organize into DMs and public threads
|
|
||||||
private _threads: MessageThread[] = [];
|
|
||||||
private _orphans: Message[] = [];
|
|
||||||
private _boosts: Message[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
const name = this.getAttribute("data-name");
|
|
||||||
this._name = name ?? "";
|
|
||||||
this.buildThreads();
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
|
||||||
switch (attr) {
|
|
||||||
case "data-viewing":
|
|
||||||
if (next != prev) {
|
|
||||||
this._viewing = next;
|
|
||||||
}
|
|
||||||
// probably only needed for profile view; we might be able to move this to child components
|
|
||||||
// keep it simple for now, but eventually we will need a way to force _view to update
|
|
||||||
break;
|
|
||||||
case "data-view":
|
|
||||||
if (!next) {
|
|
||||||
this._view = "index";
|
|
||||||
} else {
|
|
||||||
this._view = next;
|
|
||||||
}
|
|
||||||
switch (this._view) {
|
|
||||||
case "index":
|
|
||||||
this.setIdxView();
|
|
||||||
this.populateIdxView();
|
|
||||||
break;
|
|
||||||
case "thread":
|
|
||||||
this.setThreadView();
|
|
||||||
this.populateThreadView();
|
|
||||||
break;
|
|
||||||
case "profile":
|
|
||||||
this.setProfileView();
|
|
||||||
this.populateProfileView();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "data-latest":
|
|
||||||
let datastore = AdapterState._instance.data.get(this._name);
|
|
||||||
if (!datastore) {
|
|
||||||
// this shouldn't be possible
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (prev != next && next) {
|
|
||||||
this._latest = next;
|
|
||||||
}
|
|
||||||
const latestMsg = datastore.messages.get(this._latest);
|
|
||||||
if (latestMsg) {
|
|
||||||
const rootId = this.placeMsg(this._latest);
|
|
||||||
// if rootId is null, this is an orphan and we don't need to actually do any UI updates yet
|
|
||||||
if (rootId) {
|
|
||||||
switch (this._view) {
|
|
||||||
case "index":
|
|
||||||
this.updateIdxView(this._latest, rootId);
|
|
||||||
break;
|
|
||||||
case "thread":
|
|
||||||
// if the the message is part of this thread, update it
|
|
||||||
case "profile":
|
|
||||||
// if the message is from this user, show it in their profile
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const latestAuthor = datastore.profileCache.get(this._latest);
|
|
||||||
if (latestAuthor) {
|
|
||||||
switch (this._view) {
|
|
||||||
case "index":
|
|
||||||
const threadsByThisAuthor = this._threads.filter(t=>t.root.data.author == this._latest);
|
|
||||||
for (let t of threadsByThisAuthor) {
|
|
||||||
let tse = this.querySelector(`underbbs-thread-summary[data-msg='${t.root.data.id}']`)
|
|
||||||
if (tse) {
|
|
||||||
tse.setAttribute("data-author", this._latest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// also update any boosts by this author
|
|
||||||
case "thread":
|
|
||||||
case "profile":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIdxView() {
|
|
||||||
this.innerHTML = "<ul id='boost_carousel'></ul><ul id='dm_list'></ul><ul id='public_list'></ul>"
|
|
||||||
}
|
|
||||||
|
|
||||||
setThreadView() {
|
|
||||||
let html = `<a href="#${this._name}">← return to index</a>`;
|
|
||||||
html += "<ul id='msg_list'></ul>";
|
|
||||||
this.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProfileView() {
|
|
||||||
let profile_bar = util.$("profile_bar");
|
|
||||||
if (profile_bar) {
|
|
||||||
// clear any previous data
|
|
||||||
} else {
|
|
||||||
// insert the profileSidebar into the dom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
populateIdxView() {
|
|
||||||
// populate boost carousel
|
|
||||||
|
|
||||||
// skip dm list for now
|
|
||||||
// public/unified list
|
|
||||||
const pl = util.$("public_list");
|
|
||||||
if (pl) {
|
|
||||||
let html = "";
|
|
||||||
for (const t of this._threads.sort((a: MessageThread, b: MessageThread) => b.latest - a.latest)) {
|
|
||||||
html +=`<li><underbbs-thread-summary data-len="${t.messageCount}" data-adapter="${t.root.data.adapter}" data-msg="${t.root.data.id}" data-created="${t.created}" data-latest="${t.latest}"></underbbs-thread-summary></li>`;
|
|
||||||
}
|
|
||||||
pl.innerHTML = html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIdxView(latest: string, rootId: string) {
|
|
||||||
const threadSelector = `underbbs-thread-summary[data-msg='${rootId}']`
|
|
||||||
const existingThread = document.querySelector(threadSelector);
|
|
||||||
const thread = this._threads.find(t=>t.root.data.id == rootId);
|
|
||||||
if (existingThread && thread) {
|
|
||||||
existingThread.setAttribute("data-latest", `${thread.latest}`);
|
|
||||||
existingThread.setAttribute("data-len", `${thread.messageCount}`);
|
|
||||||
existingThread.setAttribute("data-new", "true");
|
|
||||||
} else {
|
|
||||||
// if latest is a boost, put it in the carousel
|
|
||||||
|
|
||||||
// unified/public list for now
|
|
||||||
const pl = util.$("public_list");
|
|
||||||
if (pl && thread) {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.innerHTML = `<underbbs-thread-summary data-len="1" data-adapter="${thread.root.data.adapter}" data-msg="${thread.root.data.id}" data-latest="${thread.latest}" data-created="${thread.created}"></underbbs-thread-summary>`;
|
|
||||||
let nextThread: Element | null = null;
|
|
||||||
for (let i = 0; i < pl.children.length; i++) {
|
|
||||||
const c = pl.children.item(i);
|
|
||||||
const latest = c?.children.item(0)?.getAttribute("data-latest")
|
|
||||||
if (latest && parseInt(latest) < thread.latest) {
|
|
||||||
nextThread = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextThread) {
|
|
||||||
nextThread.insertAdjacentElement('beforebegin', li);
|
|
||||||
} else {
|
|
||||||
pl.append(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
populateThreadView() {
|
|
||||||
}
|
|
||||||
|
|
||||||
populateProfileView() {
|
|
||||||
}
|
|
||||||
|
|
||||||
buildThreads() {
|
|
||||||
const datastore = AdapterState._instance.data.get(this._name);
|
|
||||||
if (!datastore) {
|
|
||||||
util.errMsg(this._name + " has no datastore!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// make multiple passes over the store until every message is either
|
|
||||||
// placed in a thread, the boost carousel, or orphaned and waiting for its parent to be returned
|
|
||||||
do{
|
|
||||||
for (let k of datastore.messages.keys()) {
|
|
||||||
this.placeMsg(k);
|
|
||||||
}
|
|
||||||
} while (this._threads.reduce((sum: number, thread: MessageThread)=>{
|
|
||||||
return sum + thread.messageCount;
|
|
||||||
}, 0) + this._boosts.length + this._orphans.length < datastore.messages.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
placeMsg(k: string): string | null {
|
|
||||||
const datastore = AdapterState._instance.data.get(this._name);
|
|
||||||
if (!datastore) {
|
|
||||||
util.errMsg(this._name + " has no datastore!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const msg = datastore.messages.get(k);
|
|
||||||
if (!msg) {
|
|
||||||
util.errMsg(`message [${this._name}:${k}] doesn't exist`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (msg.renoteId) {
|
|
||||||
// fetch the referent thread and put the boost in the carousel
|
|
||||||
this._convoyBatchTimer.queue(msg.renoteId, 2000);
|
|
||||||
if (!this._boosts.some(m=>m.id == msg.id)) {
|
|
||||||
this._boosts.push(msg);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (let t of this._threads) {
|
|
||||||
// avoid processing nodes again on subsequent passes
|
|
||||||
if (!msg || t.findNode(t.root, msg.id)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.replyTo) {
|
|
||||||
let x = t.addReply(msg.replyTo, msg);
|
|
||||||
if (x) {
|
|
||||||
// after adding, we try to adopt some orphans
|
|
||||||
const orphanChildren = this._orphans.filter(m=>m.replyTo == k);
|
|
||||||
for (let o of orphanChildren) {
|
|
||||||
let adopted = this.placeMsg(o.id);
|
|
||||||
if (adopted) {
|
|
||||||
this._orphans.splice(this._orphans.indexOf(o), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t.root.data.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if we made it this far, this message doesn't go in any existing thread
|
|
||||||
|
|
||||||
// if it doesn't have a parent, we can make a new thread with it
|
|
||||||
if (!msg.replyTo) {
|
|
||||||
this._threads.push(new MessageThread(msg));
|
|
||||||
// after adding, we try to adopt some orphans
|
|
||||||
const orphanChildren = this._orphans.filter(m=>m.replyTo == k);
|
|
||||||
for (let o of orphanChildren) {
|
|
||||||
let adopted = this.placeMsg(o.id);
|
|
||||||
if (adopted) {
|
|
||||||
this._orphans.splice(this._orphans.indexOf(o), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return msg.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// then, we should check if its parent is an orphan
|
|
||||||
const orphanedParent = this._orphans.find(o=>o.id == msg.replyTo);
|
|
||||||
if (orphanedParent) {
|
|
||||||
// then, try to place them both
|
|
||||||
|
|
||||||
if (this.placeMsg(orphanedParent.id)) {
|
|
||||||
this._orphans.splice(this._orphans.indexOf(orphanedParent), 1);
|
|
||||||
return this.placeMsg(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise we can orphan it and try to fill it in later
|
|
||||||
if (this._orphans.filter(o=>o.id == msg.id).length == 0) {
|
|
||||||
this._orphans.push(msg);
|
|
||||||
if (msg.replyTo) {
|
|
||||||
this._convoyBatchTimer.queue(k, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,13 +21,7 @@ export class AuthorMessagesElement 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._byAuthorTimer = new BatchTimer((ids: string[])=>{
|
this._byAuthorTimer = new BatchTimer(gateway, this._adapter, "byAuthor");
|
||||||
let url = `${gateway}/api/adapters/${this._adapter}/fetch?entity_type=byAuthor`;
|
|
||||||
for (let id of ids) {
|
|
||||||
url += `&entity_id=${id}`;
|
|
||||||
}
|
|
||||||
util.authorizedFetch("GET", url, null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
import util from './util'
|
||||||
|
|
||||||
export class BatchTimer {
|
export class BatchTimer {
|
||||||
private _batch: string[];
|
private _batch: string[];
|
||||||
private _timer: number;
|
private _timer: number;
|
||||||
private _reqFn: (id: string[])=>void;
|
private _reqFn: (id: string[])=>void;
|
||||||
|
|
||||||
constructor(reqFn: (id: string[])=>void) {
|
constructor(gateway: string, adapter: string, etype: string) {
|
||||||
this._batch = [];
|
this._batch = [];
|
||||||
this._timer = new Date().getTime();
|
this._timer = new Date().getTime();
|
||||||
this._reqFn = reqFn;
|
this._reqFn = (ids: string[])=>{
|
||||||
|
let url = `${gateway}/api/adapters/${adapter}/fetch?entity_type=${etype}`;
|
||||||
|
for (let id of ids) {
|
||||||
|
url += `&entity_id=${id}`;
|
||||||
|
}
|
||||||
|
util.authorizedFetch("GET", url, null);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public queue(id: string, timeout: number){
|
public queue(id: string, timeout: number){
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
export class BoostTileElement extends HTMLElement {
|
|
||||||
|
|
||||||
static observedAttributes = [ "data-boostid", "data-msgid", "data-author", "data-booster" ];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.innerHTML = "<div class='boost_booster'></div><div class='boost_author'></div><div class='boost_content'></div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,13 +2,11 @@ import util from "./util"
|
||||||
import {AdapterState, AdapterData} from "./adapter";
|
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 { TabBarElement } from "./tabbar-element"
|
|
||||||
import { MessageElement } from "./message-element"
|
import { MessageElement } from "./message-element"
|
||||||
import { SettingsElement } from "./settings-element"
|
import { SettingsElement } from "./settings-element"
|
||||||
import { AdapterElement } from "./adapter-element"
|
|
||||||
import { ThreadSummaryElement } from "./thread-summary-element"
|
|
||||||
import { ProfileElement } from "./profile-element"
|
import { ProfileElement } from "./profile-element"
|
||||||
import { AuthorMessagesElement } from "./author-messages-element"
|
import { AuthorMessagesElement } from "./author-messages-element"
|
||||||
|
import { TimelineElement } from "./timeline-element"
|
||||||
import {DatagramSocket} from "./websocket"
|
import {DatagramSocket} from "./websocket"
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
@ -19,21 +17,28 @@ function main() {
|
||||||
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-author-messages", AuthorMessagesElement);
|
||||||
|
customElements.define("underbbs-timeline", TimelineElement);
|
||||||
|
|
||||||
util._("closeErr", util.closeErr);
|
util._("closeErr", util.closeErr);
|
||||||
|
|
||||||
let settingsParent = util.$("settings_parent");
|
let settingsParent = util.$("settings_parent");
|
||||||
if (settingsParent) {
|
if (settingsParent) {
|
||||||
settingsParent.innerHTML = `<underbbs-settings data-adapters='${Settings._instance.adapters.map(a=>a.nickname).join(",")}'></underbbs-settings>`
|
settingsParent.innerHTML = `<underbbs-settings data-adapters='${Settings._instance.adapters.map(a=>a.nickname).join(",")}' data-gateway=""></underbbs-settings>`
|
||||||
}
|
}
|
||||||
|
|
||||||
let profileParent = util.$("profile_parent");
|
let profileParent = util.$("profile_parent");
|
||||||
if (profileParent) {
|
if (profileParent) {
|
||||||
profileParent.innerHTML = "<underbbs-profile data-adapter='honk' data-target='https://cafe.nilfm.cc/u/nilix'></underbbs-profile>"
|
profileParent.innerHTML = "<underbbs-profile data-adapter='honk' data-target='https://cafe.nilfm.cc/u/nilix' data-gateway=''></underbbs-profile>"
|
||||||
}
|
}
|
||||||
|
|
||||||
let honksParent = util.$("honks_parent");
|
let honksParent = util.$("honks_parent");
|
||||||
if (honksParent) {
|
if (honksParent) {
|
||||||
honksParent.innerHTML = "<underbbs-author-messages data-adapter='honk' data-target='https://cafe.nilfm.cc/u/nilix'></underbbs-author-messages>";
|
honksParent.innerHTML = "<underbbs-author-messages data-adapter='honk' data-target='https://cafe.nilfm.cc/u/nilix' data-gateway=''></underbbs-author-messages>";
|
||||||
|
}
|
||||||
|
|
||||||
|
let timelineParent = util.$("timeline_parent");
|
||||||
|
if (timelineParent) {
|
||||||
|
timelineParent.innerHTML = "<underbbs-timeline id='honkstream' data-adapter='honk' data-target='home' data-gateway=''></underbbs-timeline>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,7 @@ 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((ids: string[])=>{
|
this._messageTimer = new BatchTimer(gateway, this._adapter ?? "", "message");
|
||||||
let url = `${gateway}/api/adapters/${this._adapter}/fetch?entity_type=message`;
|
|
||||||
for (let id of ids) {
|
|
||||||
url += `&entity_id=${id}`;
|
|
||||||
}
|
|
||||||
util.authorizedFetch("GET", url, null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||||
|
@ -38,6 +32,8 @@ export class MessageElement extends HTMLElement {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this._id = next;
|
this._id = next;
|
||||||
|
this._message = null;
|
||||||
|
this.innerHTML = `<div class="message_metadata"></div><div class="message_content"></div><div class="message_attachments"></div>`;
|
||||||
if (this._messageTimer) {
|
if (this._messageTimer) {
|
||||||
this._messageTimer.queue(next, 100);
|
this._messageTimer.queue(next, 100);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export class Message {
|
export class Message {
|
||||||
public id: string = "";
|
public id: string = "";
|
||||||
public uri: string = "";
|
public uri: string = "";
|
||||||
|
public target: string | null = null;
|
||||||
public protocol: string = "";
|
public protocol: string = "";
|
||||||
public adapter: string = "";
|
public adapter: string = "";
|
||||||
public author: string = ""
|
public author: string = ""
|
||||||
|
@ -20,6 +21,7 @@ export class Message {
|
||||||
export class Author {
|
export class Author {
|
||||||
public id: string = "";
|
public id: string = "";
|
||||||
public uri: string = "";
|
public uri: string = "";
|
||||||
|
public target: string | null = null;
|
||||||
public protocol: string = "";
|
public protocol: string = "";
|
||||||
public adapter: string = "";
|
public adapter: string = "";
|
||||||
public name: string = "";
|
public name: string = "";
|
||||||
|
|
|
@ -24,13 +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((ids: string[])=>{
|
this._authorTimer = new BatchTimer(gateway, this._adapter, "author");
|
||||||
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) {
|
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||||
|
|
16
frontend/ts/subscriber.ts
Normal file
16
frontend/ts/subscriber.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import util from './util';
|
||||||
|
|
||||||
|
export class Subscriber {
|
||||||
|
private _reqFn: (filter: string)=>void;
|
||||||
|
|
||||||
|
constructor(gateway: string, adapter: string, target: string | null) {
|
||||||
|
this._reqFn = (filter: string) => {
|
||||||
|
let url = `${gateway}/api/adapters/${adapter}/subscribe`
|
||||||
|
util.authorizedFetch("POST", url, JSON.stringify({filter, target}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(filter: string) {
|
||||||
|
this._reqFn(filter);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,84 +0,0 @@
|
||||||
import util from "./util"
|
|
||||||
import {Settings} from "./settings"
|
|
||||||
|
|
||||||
export class TabBarElement extends HTMLElement {
|
|
||||||
static observedAttributes = [ "data-adapters", "data-currentadapter" ]
|
|
||||||
|
|
||||||
private _adapters: string[] = [];
|
|
||||||
private _currentAdapter: string | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
if (this._currentAdapter) {
|
|
||||||
this.showAdapterFunc(this, this._currentAdapter)();
|
|
||||||
} else {
|
|
||||||
this.showSettings(this)();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
|
||||||
switch (attr) {
|
|
||||||
case "data-adapters":
|
|
||||||
if (next) {
|
|
||||||
this._adapters = next.split(",")
|
|
||||||
} else {
|
|
||||||
this._adapters = [];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "data-currentadapter":
|
|
||||||
this._currentAdapter = next || null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = "<ul><li><a id='tabbar_settings' href='#settings'>settings</a></li>";
|
|
||||||
|
|
||||||
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 = util.$("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 = util.$(`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 = util.$("mainarea_injectparent");
|
|
||||||
if (x) {
|
|
||||||
x.innerHTML = `<underbbs-settings data-adapters=${Settings._instance.adapters.map(a=>a.nickname).join(",") ?? []}></underbbs-settings>`;
|
|
||||||
self.setAttribute("data-currentadapter", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showAdapterFunc(self: TabBarElement, adapter: string): ()=>void {
|
|
||||||
return ()=>{
|
|
||||||
let x = util.$("mainarea_injectparent");
|
|
||||||
if (x) {
|
|
||||||
x.innerHTML = `<underbbs-adapter id="adapter_${adapter}" data-name="${adapter}" data-view=""></underbbs-adapter>`;
|
|
||||||
self.setAttribute("data-currentadapter", adapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
import util from "./util"
|
|
||||||
import { Message, Author } from "./message"
|
|
||||||
import { AdapterState } from "./adapter"
|
|
||||||
import { BatchTimer } from "./batch-timer"
|
|
||||||
|
|
||||||
export class ThreadSummaryElement extends HTMLElement {
|
|
||||||
static observedAttributes = [ "data-msg", "data-len", "data-author", "data-created", "data-latest", "data-new" ];
|
|
||||||
|
|
||||||
private _len: number = 0;;
|
|
||||||
private _msg: Message | null = null;;
|
|
||||||
private _author: Author | null = null;
|
|
||||||
private _adapter: string = "";
|
|
||||||
private _created: number = 0;
|
|
||||||
private _latest: number = 0;
|
|
||||||
private _new: boolean = false;
|
|
||||||
|
|
||||||
private _authorTimer: BatchTimer;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.innerHTML = "<div class='thread_summary'><div class='thread_author'></div><div class='thread_text'></div><div class='thread_metadata'></div></div>"
|
|
||||||
|
|
||||||
// adapter shouldn't change, just set it here
|
|
||||||
this._adapter = this.getAttribute("data-adapter") ?? "";
|
|
||||||
this.addEventListener("click", this.viewThread(this), false);
|
|
||||||
this._authorTimer = new BatchTimer((ids: string[])=>{
|
|
||||||
let url = `/api/adapters/${this._adapter}/fetch?entity_type=author`;
|
|
||||||
for (let id of ids) {
|
|
||||||
url += `&entity_id=${id}`;
|
|
||||||
}
|
|
||||||
util.authorizedFetch("GET", url, null)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
|
||||||
|
|
||||||
const datastore = AdapterState._instance.data.get(this._adapter);
|
|
||||||
if (!datastore) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let metadataChanged = false;
|
|
||||||
|
|
||||||
switch (attr) {
|
|
||||||
case "data-msg":
|
|
||||||
if (next && next != prev) {
|
|
||||||
this._msg = datastore.messages.get(next) || null;
|
|
||||||
if (this._msg) {
|
|
||||||
// TODO: use attachment alttext or fallback text if no msg content
|
|
||||||
// TODO: handle boosts, quotes properly
|
|
||||||
|
|
||||||
const threadText = this.querySelector(".thread_text");
|
|
||||||
if (threadText) {
|
|
||||||
threadText.innerHTML = this._msg.content;
|
|
||||||
}
|
|
||||||
this.setAttribute("data-author", this._msg.author);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "data-author":
|
|
||||||
if (next) {
|
|
||||||
let authorData= datastore.profileCache.get(next);
|
|
||||||
if (!authorData) {
|
|
||||||
this._authorTimer.queue(next, 2000);
|
|
||||||
this._author = <Author>{id: next};
|
|
||||||
const threadAuthor = this.querySelector(".thread_author");
|
|
||||||
if (threadAuthor) {
|
|
||||||
threadAuthor.innerHTML = `<a id="thread_${this._adapter}_${(this._msg ? this._msg.id : 0)}_${this._author.id}" href="#author?id=${this._author.id}">${this._author.id}</a>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._author = authorData;
|
|
||||||
const threadAuthor = this.querySelector(".thread_author");
|
|
||||||
if (threadAuthor) {
|
|
||||||
threadAuthor.innerHTML = `<img src="${this._author.profilePic}" alt="${this._author.id}"/> <a id="thread_${this._adapter}_${(this._msg ? this._msg.id : 0)}_${this._author.id}" href="#author?id=${this._author.id}">${this._author.id}</a>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "data-len":
|
|
||||||
this._len = parseInt(next);
|
|
||||||
metadataChanged = true;
|
|
||||||
break;
|
|
||||||
case "data-latest":
|
|
||||||
this._latest = parseInt(next);
|
|
||||||
metadataChanged = true;
|
|
||||||
break;
|
|
||||||
case "data-new":
|
|
||||||
this._new = next ? true : false;
|
|
||||||
metadataChanged = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadataChanged) {
|
|
||||||
const threadMeta = this.querySelector(".thread_metadata");
|
|
||||||
if (threadMeta) {
|
|
||||||
threadMeta.innerHTML = `<span>${this._new ? "!" : ""}[${this._len}] created: ${new Date(this._created)}, updated: ${new Date(this._latest)}</span>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewThread(self: ThreadSummaryElement) {
|
|
||||||
return () => {
|
|
||||||
const a = util.$(`adapter_${self._adapter}`);
|
|
||||||
if (a && self._msg) {
|
|
||||||
a.setAttribute("data-view", "thread");
|
|
||||||
a.setAttribute("data-viewing", self._msg.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
91
frontend/ts/timeline-element.ts
Normal file
91
frontend/ts/timeline-element.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { Author, Message } from "./message"
|
||||||
|
import util from "./util"
|
||||||
|
import { Subscriber } from "./subscriber"
|
||||||
|
import { AdapterState } from "./adapter"
|
||||||
|
|
||||||
|
export class TimelineElement extends HTMLElement {
|
||||||
|
static observedAttributes = [ "data-latest", "data-adapter", "data-target" ];
|
||||||
|
|
||||||
|
private _timeline: string | null = null;
|
||||||
|
private _adapter: string = "";
|
||||||
|
|
||||||
|
private _messages: Message[] = [];
|
||||||
|
private _subscriber: Subscriber | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.innerHTML = `<ul class="messages_list"></ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this._timeline = this.getAttribute("data-target");
|
||||||
|
this._adapter = this.getAttribute("data-adapter") ?? "";
|
||||||
|
const gateway = this.getAttribute("data-gateway") ?? "";
|
||||||
|
this._subscriber = new Subscriber(gateway, this._adapter ?? "", this.getAttribute("id") ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||||
|
switch (attr) {
|
||||||
|
case "data-target":
|
||||||
|
if (!next) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._timeline = next;
|
||||||
|
this.innerHTML = `<ul class="messages_list"></ul>`;
|
||||||
|
if (this._subscriber) {
|
||||||
|
this._subscriber.subscribe(next);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
console.log(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}"></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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,11 @@ export class DatagramSocket {
|
||||||
const target = f.getAttribute("data-target");
|
const target = f.getAttribute("data-target");
|
||||||
f.setAttribute("data-target", target ?? "");
|
f.setAttribute("data-target", target ?? "");
|
||||||
});
|
});
|
||||||
|
const timelines = document.querySelectorAll("underbbs-timeline");
|
||||||
|
timelines.forEach(t=>{
|
||||||
|
const target = t.getAttribute("data-target");
|
||||||
|
t.setAttribute("data-target", target ?? "");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
@ -57,7 +62,6 @@ export class DatagramSocket {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(store);
|
|
||||||
// go through each type of component and give it the latest if it's relevant to them
|
// go through each type of component and give it the latest if it's relevant to them
|
||||||
let profileTargets = document.querySelectorAll(`underbbs-profile[data-adapter="${data.adapter}"][data-target="${data.id}"]`);
|
let profileTargets = document.querySelectorAll(`underbbs-profile[data-adapter="${data.adapter}"][data-target="${data.id}"]`);
|
||||||
profileTargets.forEach(t=>{
|
profileTargets.forEach(t=>{
|
||||||
|
@ -74,6 +78,14 @@ export class DatagramSocket {
|
||||||
t.setAttribute("data-latest", data.renoteId);
|
t.setAttribute("data-latest", data.renoteId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (data.target) {
|
||||||
|
console.log("data has target: " + data.target);
|
||||||
|
let e = document.querySelector(`underbbs-timeline#${data.target}[data-adapter="${data.adapter}"]`);
|
||||||
|
if (e) {
|
||||||
|
console.log("setting latest...")
|
||||||
|
e.setAttribute("data-latest", data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,22 +94,7 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("adapter initialized - subscribing with default filter")
|
log.Print("adapter initialized; adding to array")
|
||||||
|
|
||||||
errs := a.Subscribe(a.DefaultSubscriptionFilter())
|
|
||||||
if errs != nil {
|
|
||||||
errMsg := ""
|
|
||||||
for _, e := range errs {
|
|
||||||
log.Print("processing an error")
|
|
||||||
errMsg += fmt.Sprintf("- %s\n", e.Error())
|
|
||||||
}
|
|
||||||
util.AddContextValue(req, "data", errMsg)
|
|
||||||
w.WriteHeader(500)
|
|
||||||
next.ServeHTTP(w, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Print("adapter ready for use; adding to array")
|
|
||||||
|
|
||||||
adapters = append(adapters, a)
|
adapters = append(adapters, a)
|
||||||
log.Print("adapter added to array")
|
log.Print("adapter added to array")
|
||||||
|
@ -136,9 +121,42 @@ func apiGetAdapters(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiAdapterSubscribe(next http.Handler) http.Handler {
|
type subscribeParams struct {
|
||||||
|
Filter string `json:"filter"`
|
||||||
|
Target *string `json:"target,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
w.WriteHeader(201)
|
// get subscriber key
|
||||||
|
skey, err := getSubscriberKey(req)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subscriber := getSubscriberByKey(skey, subscribers)
|
||||||
|
if subscriber == nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters := subscribers[subscriber]
|
||||||
|
|
||||||
|
urlParams := req.Context().Value("params").(map[string]string)
|
||||||
|
adapter := urlParams["adapter_id"]
|
||||||
|
sp := subscribeParams{}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&sp)
|
||||||
|
for _, a := range adapters {
|
||||||
|
if a.Name() == adapter {
|
||||||
|
fmt.Printf("adapter.subscribe call: %s {%s, %s}\n", adapter, sp.Filter, *sp.Target)
|
||||||
|
a.Subscribe(sp.Filter, sp.Target)
|
||||||
|
|
||||||
|
w.WriteHeader(201)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(404)
|
||||||
next.ServeHTTP(w, req)
|
next.ServeHTTP(w, req)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -205,8 +223,8 @@ func (self *BBSServer) apiMux() http.Handler {
|
||||||
))
|
))
|
||||||
|
|
||||||
// adapters/:name/subscribe
|
// adapters/:name/subscribe
|
||||||
rtr.Post(`/adapters/(?P<id>\S+)/subscribe`, ProtectWithSubscriberKey(
|
rtr.Post(`/adapters/(?P<adapter_id>\S+)/subscribe`, ProtectWithSubscriberKey(
|
||||||
apiAdapterSubscribe(renderer.JSON("data")),
|
apiAdapterSubscribe(renderer.JSON("data"), self.subscribers),
|
||||||
self.subscribers,
|
self.subscribers,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ module.exports = {
|
||||||
context: path.resolve(__dirname, 'frontend', '.js'),
|
context: path.resolve(__dirname, 'frontend', '.js'),
|
||||||
entry: {
|
entry: {
|
||||||
main: './index.js',
|
main: './index.js',
|
||||||
serviceWorker: './serviceWorker.js',
|
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
|
|
Loading…
Reference in a new issue