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