diff --git a/adapter/honk.go b/adapter/honk.go
index d416346..d41476e 100644
--- a/adapter/honk.go
+++ b/adapter/honk.go
@@ -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
diff --git a/frontend/ts/author-messages-element.ts b/frontend/ts/author-messages-element.ts
index 9f54389..d9c0081 100644
--- a/frontend/ts/author-messages-element.ts
+++ b/frontend/ts/author-messages-element.ts
@@ -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 = ``
+ e.innerHTML = ``
// second pass, try to place it in reverse-chronological order
for (let i = 0; i < ul.childElementCount; i++){
const id = ul.children[i]?.children[0]?.getAttribute("data-target");
const ogMsg = this._messages.find(m=>(m.renoteId ?? m.id) == id);
- if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) < (msg.renoteTime ?? msg.created)) {
+ if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) <= (msg.renoteTime ?? msg.created)) {
ul.insertBefore(e, ul.children[i])
e.children[0].setAttribute("data-latest", next);
return;
diff --git a/frontend/ts/create-message-element.ts b/frontend/ts/create-message-element.ts
new file mode 100644
index 0000000..0103401
--- /dev/null
+++ b/frontend/ts/create-message-element.ts
@@ -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 = `
`
+ }
+
+ 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
+ });;
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/ts/doer.ts b/frontend/ts/doer.ts
new file mode 100644
index 0000000..6b14334
--- /dev/null
+++ b/frontend/ts/doer.ts
@@ -0,0 +1,16 @@
+import util from './util';
+
+export class Doer {
+ private _reqFn: (doParams: any) => Promise;
+
+ 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 {
+ return this._reqFn(doParams);
+ }
+}
\ No newline at end of file
diff --git a/frontend/ts/message-element.ts b/frontend/ts/message-element.ts
index 358659c..cc5fe6d 100644
--- a/frontend/ts/message-element.ts
+++ b/frontend/ts/message-element.ts
@@ -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 = ``
+ this.innerHTML = ``
}
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 = ``;
+ this.innerHTML = ``;
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 = `${this._message.renoter}${new Date(this._message.renoteTime ?? 0)}`
@@ -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 = "
";
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 += "
";
attachments.innerHTML = html;
}
+ if (this._interactable && interactions) {
+ interactions.innerHTML = `permalink`
+ 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
+ }
}
\ No newline at end of file
diff --git a/frontend/ts/timeline-element.ts b/frontend/ts/timeline-element.ts
index 2a4f0d8..472b1fb 100644
--- a/frontend/ts/timeline-element.ts
+++ b/frontend/ts/timeline-element.ts
@@ -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 = ``
+ e.innerHTML = ``
// second pass, try to place it in reverse-chronological order
for (let i = 0; i < ul.childElementCount; i++){
const id = ul.children[i]?.children[0]?.getAttribute("data-target");
const ogMsg = this._messages.find(m=>(m.renoteId ?? m.id) == id);
- if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) < (msg.renoteTime ?? msg.created)) {
+ if (ogMsg && (ogMsg.renoteTime ?? ogMsg.created) <= (msg.renoteTime ?? msg.created)) {
ul.insertBefore(e, ul.children[i])
e.children[0].setAttribute("data-latest", next);
return;
diff --git a/frontend/ts/websocket.ts b/frontend/ts/websocket.ts
index 31fd876..bff8e2f 100644
--- a/frontend/ts/websocket.ts
+++ b/frontend/ts/websocket.ts
@@ -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!")
}
diff --git a/server/api.go b/server/api.go
index 55cd93f..c54a467 100644
--- a/server/api.go
+++ b/server/api.go
@@ -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\S+)/do`, ProtectWithSubscriberKey(
+ rtr.Post(`/adapters/(?P\S+)/do`, ProtectWithSubscriberKey(
apiAdapterDo(renderer.JSON("data"), self.subscribers),
self.subscribers,
))