tinyrelayfile
tinyrelay/tinyclient/chat-activity.js/

tinyclient/chat-activity.js raw
1(() => {
2 "use strict";
3 const tiny=globalThis.tiny;
4 class ChatActivity extends HTMLElement {
5 connectedCallback() {
6 if (this.running) return;
7 this.running=true; this.stopped=false; this.version="";
8 this.onEvent=e=>{ if (e.detail?.room===this.getAttribute("room")) this.schedule(); };
9 this.onVisibility=()=>{ if (!document.hidden) this.refresh(); };
10 this.onLogout=()=>{ this.stopped=true; this.abort?.(); this.replaceChildren(); };
11 this.onConfig=e=>{
12 const cfg=e.detail?.cfg;
13 if(e.target!==this || cfg?.trigger?.target!==this) return;
14 const force=this.forceRefresh;
15 cfg.credentials="same-origin"; cfg.cache="no-store"; cfg.transition=false;
16 cfg.headers={...cfg.headers,Accept:"text/html"};
17 this.abort=cfg.abort;
18 cfg.swap=()=>this.swap(cfg,force);
19 };
20 this.onFinally=e=>{ if(e.target===this) { this.loading=false; this.abort=null; queueMicrotask(()=>{this.resolveRefresh?.();this.resolveRefresh=null;}); } };
21 this.onError=e=>{ if(e.target===this && e.detail?.error?.name!=="AbortError") this.setAttribute("title","Chat activity could not be refreshed."); };
22 document.addEventListener("tiny:room-event",this.onEvent);
23 document.addEventListener("visibilitychange",this.onVisibility);
24 document.addEventListener("tiny:logout",this.onLogout);
25 this.addEventListener("fx:config",this.onConfig);
26 this.addEventListener("fx:finally",this.onFinally);
27 this.addEventListener("fx:error",this.onError);
28 this.timer=setInterval(()=>this.refresh(),15000);
29 queueMicrotask(()=>{if(this.isConnected && !this.stopped){this.dispatchEvent(new CustomEvent("fx:process",{bubbles:true}));this.refresh();}});
30 }
31 disconnectedCallback() {
32 this.running=false;this.stopped=true;this.abort?.();clearInterval(this.timer);clearTimeout(this.debounce);
33 document.removeEventListener("tiny:room-event",this.onEvent);document.removeEventListener("visibilitychange",this.onVisibility);document.removeEventListener("tiny:logout",this.onLogout);
34 this.removeEventListener("fx:config",this.onConfig);this.removeEventListener("fx:finally",this.onFinally);this.removeEventListener("fx:error",this.onError);
35 this.resolveRefresh?.();this.resolveRefresh=null;this.loading=false;
36 }
37 schedule() { if(!this.debounce)this.debounce=setTimeout(()=>{this.debounce=null;this.refresh();},400); }
38 async read() {
39 const url=new URL(tiny.localPath(this.getAttribute("endpoint")),location.href);
40 if(url.origin!==location.origin)throw Error("Invalid activity address.");
41 const response=await fetch(url,{credentials:"same-origin",cache:"no-store"});
42 if(!response.ok)throw Error("Chat activity could not be refreshed.");
43 return response.text();
44 }
45 async refresh(force=false) {
46 if(this.loading || this.stopped || document.hidden || (!force && this.contains(document.activeElement)) || !this.__fixi)return;
47 this.loading=true;this.forceRefresh=force;
48 return new Promise(resolve=>{this.resolveRefresh=resolve;this.dispatchEvent(new CustomEvent("tiny:activity-refresh"));});
49 }
50 swap(cfg,force=false) {
51 if(this.stopped || !this.isConnected || !cfg.response.ok || cfg.text===this.version || (!force && this.contains(document.activeElement)))return;
52 const opened=new Set([...this.querySelectorAll("details[open][data-task]")].map(el=>el.dataset.task));
53 morph(cfg.target,cfg.text);
54 cfg.target.querySelectorAll("details[data-task]").forEach(el=>{el.open=opened.has(el.dataset.task);});
55 cfg.target.querySelectorAll("chat-decision").forEach(el=>{if(!el.form?.isConnected){el.form=null;el.output=null;el.connectedCallback();}});
56 this.version=cfg.text;
57 }
58 }
59 class ChatDecision extends tiny.ui.FormElement {
60 connectedCallback() { if(this.form) return; super.connectedCallback(); }
61 async submit(form) {
62 const host=this.closest("chat-activity"),id=this.getAttribute("event"),author=this.getAttribute("pubkey"),actor=this.getAttribute("actor"),room=this.getAttribute("room");
63 const signer=tiny.signer();
64 if(!signer || await signer.getPublicKey()!==actor || tiny.signer()!==signer) throw Error("Connect the signer for this account.");
65 const fresh=document.createElement("template"); fresh.innerHTML=await host.read();
66 const allowed=[...fresh.content.querySelectorAll("chat-decision")].find(el=>el.getAttribute("event")===id && el.getAttribute("actor")===actor && el.getAttribute("pubkey")===author);
67 if(!allowed) { host.schedule(); throw Error("This request is no longer waiting for your answer."); }
68 let unsigned;
69 if(this.hasAttribute("question")) {
70 const content=(form.elements.content?.value || "").trim(); if(!content) throw Error("Write an answer first.");
71 unsigned={kind:1111,created_at:Math.floor(Date.now()/1000),tags:[["h",room],["E",id,"",author],["K",this.getAttribute("kind")],["P",author],["e",id,"",author],["k",this.getAttribute("kind")],["p",author]],content};
72 } else {
73 const choice=this.submitter?.value;
74 if(!["+","-"].includes(choice)) throw Error("Choose Approve or Deny.");
75 unsigned={kind:7,created_at:Math.floor(Date.now()/1000),tags:[["h",room],["e",id,"",author],["p",author],["k",this.getAttribute("kind")]],content:choice};
76 }
77 this.report("Signing…"); await tiny.signing.publish(unsigned);
78 this.report("Answer sent."); this.querySelectorAll("button,textarea").forEach(el=>el.disabled=true);
79 await host.refresh(true);
80 }
81 }
82 customElements.define("chat-activity",ChatActivity); customElements.define("chat-decision",ChatDecision);
83})();

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs