add CreateMessageElement and enable interactivity on messages (replies)
This commit is contained in:
parent
264be94427
commit
2ff69b9d38
8 changed files with 186 additions and 12 deletions
|
@ -248,6 +248,8 @@ func (self *HonkAdapter) donk(data map[string]string) error {
|
|||
fieldName = "donkdesc"
|
||||
case "content":
|
||||
fieldName = "noise"
|
||||
case "replyto":
|
||||
fieldName = "rid"
|
||||
}
|
||||
if fw, err = w.CreateFormField(fieldName); err != nil {
|
||||
return err
|
||||
|
@ -292,6 +294,12 @@ func (self *HonkAdapter) Do(action string, data map[string]string) error {
|
|||
if exists {
|
||||
return self.donk(data)
|
||||
}
|
||||
|
||||
replyto, exists := data["replyto"]
|
||||
if exists {
|
||||
honkForm["rid"] = []string{replyto}
|
||||
}
|
||||
|
||||
res, err := http.PostForm(self.server+"/api", honkForm)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -9,6 +9,8 @@ export class AuthorMessagesElement extends HTMLElement {
|
|||
private _id: string | null = null;
|
||||
private _adapter: string = "";
|
||||
|
||||
private _interactable: boolean = false;
|
||||
|
||||
private _messages: Message[] = [];
|
||||
private _byAuthorTimer: BatchTimer | null = null;
|
||||
|
||||
|
@ -22,6 +24,7 @@ export class AuthorMessagesElement extends HTMLElement {
|
|||
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) {
|
||||
|
@ -43,7 +46,7 @@ export class AuthorMessagesElement extends HTMLElement {
|
|||
}
|
||||
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)));
|
||||
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) {
|
||||
|
@ -67,12 +70,12 @@ export class AuthorMessagesElement 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}"></underbbs-message>`
|
||||
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)) {
|
||||
if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) <= (msg.renoteTime ?? msg.created)) {
|
||||
ul.insertBefore(e, ul.children[i])
|
||||
e.children[0].setAttribute("data-latest", next);
|
||||
return;
|
||||
|
|
101
frontend/ts/create-message-element.ts
Normal file
101
frontend/ts/create-message-element.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { Doer } from './doer'
|
||||
|
||||
export class CreateMessageElement extends HTMLElement {
|
||||
private _gateway: string = "";
|
||||
private _adapter: string = ""
|
||||
private _doer: Doer | null = null;
|
||||
static observedAttributes = [ "data-replyto" ];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.innerHTML = `<form onsubmit="return false"><input readonly class="createmessage_replyto"></input><textarea class="createmessage_content"></textarea><details><summary>attachment?</summary><input type="file" class="createmessage_file"></input><input class="createmessage_desc"></input></details><input type="submit" value="post"></input></form>`
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._gateway = this.getAttribute("data-gateway") ?? "";
|
||||
this._adapter = this.getAttribute("data-adapter") ?? "";
|
||||
this._doer = new Doer(this._gateway, this._adapter);
|
||||
|
||||
const postBtn = this.querySelector(`input[type="submit"]`);
|
||||
if (postBtn) {
|
||||
postBtn.addEventListener("click", this.doPost.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||
switch (attr) {
|
||||
// when we implement replies, we'll come back here
|
||||
case "data-replyto":
|
||||
const e = this.querySelector(".createmessage_replyto") as HTMLInputElement;
|
||||
if (e) {
|
||||
e.value = next;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resetInput(r: Response) {
|
||||
if (r.ok) {
|
||||
const f = this.querySelector("form") as HTMLFormElement;
|
||||
if (f) {
|
||||
f.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doPost() {
|
||||
const msgContent = this.querySelector(".createmessage_content") as HTMLTextAreaElement;
|
||||
const msgAttachment = this.querySelector(".createmessage_file") as HTMLInputElement;
|
||||
const msgAttachmentDesc = this.querySelector(".createmessage_desc") as HTMLInputElement;
|
||||
const msgReplyTo = this.querySelector(".createmessage_replyto") as HTMLInputElement;
|
||||
|
||||
const doReq: any = {};
|
||||
doReq.action = "post";
|
||||
doReq.content = msgContent.value ?? "";
|
||||
|
||||
if (msgReplyTo && msgReplyTo.value) {
|
||||
doReq.replyto = msgReplyTo.value;
|
||||
}
|
||||
|
||||
if (msgAttachment.files && msgAttachment.files[0]) {
|
||||
const r = new FileReader();
|
||||
const self = this;
|
||||
|
||||
doReq.desc = msgAttachmentDesc.value ?? "";
|
||||
r.onload = ()=>{
|
||||
if (self && self._doer) {
|
||||
doReq.file = r.result;
|
||||
self._doer.do(doReq)
|
||||
.then(r=>{
|
||||
if (r.ok) {
|
||||
msgContent.value = '';
|
||||
msgAttachment.value = '';
|
||||
msgAttachmentDesc.value = '';
|
||||
msgReplyTo.value = '';
|
||||
}
|
||||
})
|
||||
.catch(err=>{
|
||||
// try to display an error locally on the component
|
||||
});
|
||||
}
|
||||
}
|
||||
r.readAsDataURL(msgAttachment.files[0]);
|
||||
return;
|
||||
}
|
||||
if (this._doer) {
|
||||
this._doer.do(doReq)
|
||||
.then(r=>{
|
||||
if (r.ok) {
|
||||
msgContent.value = '';
|
||||
msgAttachment.value = '';
|
||||
msgAttachmentDesc.value = '';
|
||||
msgReplyTo.value = '';
|
||||
}
|
||||
})
|
||||
.catch(err=>{
|
||||
// try to display an error locally on the component
|
||||
});;
|
||||
}
|
||||
}
|
||||
}
|
16
frontend/ts/doer.ts
Normal file
16
frontend/ts/doer.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import util from './util';
|
||||
|
||||
export class Doer {
|
||||
private _reqFn: (doParams: any) => Promise<Response>;
|
||||
|
||||
constructor(gateway: string, adapter: string) {
|
||||
this._reqFn = (doParams: any) => {
|
||||
let url = `${gateway}/api/adapters/${adapter}/do`;
|
||||
return util.authorizedFetch("POST", url, JSON.stringify(doParams));
|
||||
}
|
||||
}
|
||||
|
||||
do(doParams: any): Promise<Response> {
|
||||
return this._reqFn(doParams);
|
||||
}
|
||||
}
|
|
@ -10,12 +10,14 @@ export class MessageElement extends HTMLElement {
|
|||
private _adapter: string | null = null;
|
||||
|
||||
private _message: Message | null = null;
|
||||
private _interactable: boolean = false;
|
||||
private _replyWith: string | null = null;
|
||||
|
||||
private _messageTimer: BatchTimer | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.innerHTML = `<div class="message_metadata"></div><div class="message_content"></div><div class="message_attachments"></div>`
|
||||
this.innerHTML = `<div class="message_metadata"></div><div class="message_content"></div><div class="message_attachments"></div><div class="message_interactions"></div>`
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
@ -23,6 +25,8 @@ export class MessageElement extends HTMLElement {
|
|||
this._adapter = this.getAttribute("data-adapter");
|
||||
const gateway = this.getAttribute("data-gateway") ?? "";
|
||||
this._messageTimer = new BatchTimer(gateway, this._adapter ?? "", "message");
|
||||
this._interactable = this.getAttribute("data-interactable") != null;
|
||||
this._replyWith = this.getAttribute("data-replywith");
|
||||
}
|
||||
|
||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||
|
@ -33,7 +37,7 @@ 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>`;
|
||||
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);
|
||||
}
|
||||
|
@ -51,6 +55,7 @@ export class MessageElement extends HTMLElement {
|
|||
const metadata = this.querySelector(".message_metadata");
|
||||
const content = this.querySelector(".message_content");
|
||||
const attachments = this.querySelector(".message_attachments");
|
||||
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>`
|
||||
|
@ -62,7 +67,7 @@ export class MessageElement extends HTMLElement {
|
|||
if (content) {
|
||||
content.innerHTML = this._message.content;
|
||||
}
|
||||
if (attachments && this._message.attachments.length > 0) {
|
||||
if (attachments && this._message.attachments && this._message.attachments.length > 0) {
|
||||
let html = "<ul>";
|
||||
for (const a of this._message.attachments) {
|
||||
// we can do it based on actual mimetype later but now let's just do an extension check
|
||||
|
@ -104,8 +109,34 @@ export class MessageElement extends HTMLElement {
|
|||
html += "</ul>";
|
||||
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>`
|
||||
const replyBtn = this.querySelector(".message_reply");
|
||||
const boostBtn = this.querySelector(".message_boost");
|
||||
if (replyBtn) {
|
||||
replyBtn.addEventListener("click", this.reply.bind(this));
|
||||
}
|
||||
if (boostBtn) {
|
||||
boostBtn.addEventListener("click", this.boost.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private reply() {
|
||||
const e = document.querySelector(`#${this._replyWith}`);
|
||||
if (e) {
|
||||
e.setAttribute("data-replyto", this._id || "");
|
||||
const txtArea = e.querySelector("textarea") as HTMLTextAreaElement;
|
||||
if (txtArea) {
|
||||
txtArea.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boost() {
|
||||
// use a Doer to boost
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@ export class TimelineElement extends HTMLElement {
|
|||
private _timeline: string | null = null;
|
||||
private _adapter: string = "";
|
||||
|
||||
private _interactable: boolean = false;
|
||||
private _replyWith: string | null = null;
|
||||
|
||||
private _messages: Message[] = [];
|
||||
private _subscriber: Subscriber | null = null;
|
||||
|
||||
|
@ -22,6 +25,8 @@ 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._interactable = this.getAttribute("data-interactable") != null;
|
||||
this._replyWith = this.getAttribute("data-replywith");
|
||||
}
|
||||
|
||||
attributeChangedCallback(attr: string, prev: string, next: string) {
|
||||
|
@ -46,7 +51,7 @@ export class TimelineElement extends HTMLElement {
|
|||
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)));
|
||||
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) {
|
||||
|
@ -70,12 +75,12 @@ 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}"></underbbs-message>`
|
||||
e.innerHTML = `<underbbs-message data-adapter="${this._adapter}" data-target="${next}" ${this._interactable ? "data-interactable" : ""} ${this._replyWith ? "data-replywith='" + this._replyWith + "'" : ""}></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)) {
|
||||
if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) <= (msg.renoteTime ?? msg.created)) {
|
||||
ul.insertBefore(e, ul.children[i])
|
||||
e.children[0].setAttribute("data-latest", next);
|
||||
return;
|
||||
|
|
|
@ -8,6 +8,7 @@ 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"
|
||||
|
||||
export class DatagramSocket {
|
||||
public static skey: string | null = null;
|
||||
|
@ -132,6 +133,7 @@ function init() {
|
|||
customElements.define("underbbs-author-messages", AuthorMessagesElement);
|
||||
customElements.define("underbbs-timeline", TimelineElement);
|
||||
customElements.define("underbbs-timeline-filter", TimelineFilterElement);
|
||||
customElements.define("underbbs-create-message", CreateMessageElement);
|
||||
|
||||
console.log("underbbs initialized!")
|
||||
}
|
||||
|
|
|
@ -223,14 +223,22 @@ func apiAdapterDo(next http.Handler, subscribers map[*Subscriber][]adapter.Adapt
|
|||
return
|
||||
}
|
||||
if f, exists := doReq["file"]; exists {
|
||||
rawFile, err := base64.StdEncoding.DecodeString(f)
|
||||
firstCommaIdx := strings.Index(f, ",")
|
||||
rawFile, err := base64.StdEncoding.DecodeString(f[firstCommaIdx+1:])
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
doReq["file"] = string(rawFile)
|
||||
}
|
||||
a.Do(doReq["action"], doReq)
|
||||
action, ok := doReq["action"]
|
||||
if ok {
|
||||
delete(doReq, "action")
|
||||
} else {
|
||||
w.WriteHeader(422)
|
||||
return
|
||||
}
|
||||
a.Do(action, doReq)
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
@ -265,7 +273,7 @@ func (self *BBSServer) apiMux() http.Handler {
|
|||
self.subscribers,
|
||||
))
|
||||
|
||||
rtr.Post(`adapters/(?P<adapter-id>\S+)/do`, ProtectWithSubscriberKey(
|
||||
rtr.Post(`/adapters/(?P<adapter_id>\S+)/do`, ProtectWithSubscriberKey(
|
||||
apiAdapterDo(renderer.JSON("data"), self.subscribers),
|
||||
self.subscribers,
|
||||
))
|
||||
|
|
Loading…
Reference in a new issue