diff --git a/components/ILIAS/Chatroom/classes/BuildChat.php b/components/ILIAS/Chatroom/classes/BuildChat.php index 719b85b9b7d9..ce9a8207a109 100644 --- a/components/ILIAS/Chatroom/classes/BuildChat.php +++ b/components/ILIAS/Chatroom/classes/BuildChat.php @@ -29,6 +29,8 @@ use ilTemplate; use ilObjUser; use ilCalendarSettings; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; class BuildChat { @@ -38,7 +40,9 @@ public function __construct( private readonly ilChatroomObjectGUI $gui, private readonly ilChatroom $room, private readonly ilChatroomServerSettings $settings, - private readonly ilObjUser $user + private readonly ilObjUser $user, + private readonly UIFactory $ui_factory, + private readonly UIRenderer $ui_renderer, ) { } @@ -56,6 +60,7 @@ public function template(bool $read_only, array $initial, string $input, string $set_json_var('INITIAL_USERS', $this->room->getConnectedUsers()); $set_json_var('DATE_FORMAT', (string) $this->user->getDateFormat()); $set_json_var('TIME_FORMAT', $this->timeFormat()); + $set_json_var('NOTHING_FOUND', $this->ui_renderer->render($this->ui_factory->messageBox()->info($this->ilLng->txt('chat_osc_no_usr_found')))); $room_tpl->setVariable('CHAT_OUTPUT', $output); $room_tpl->setVariable('CHAT_INPUT', $input); diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php index f7b60f4b9d73..bbee094e9fed 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php @@ -502,6 +502,6 @@ private function buildUserActions(int $user_id, array $actions): array private function buildChat(ilChatroom $room, ilChatroomServerSettings $settings): BuildChat { - return new BuildChat($this->ilCtrl, $this->ilLng, $this->gui, $room, $settings, $this->ilUser); + return new BuildChat($this->ilCtrl, $this->ilLng, $this->gui, $room, $settings, $this->ilUser, $this->uiFactory, $this->uiRenderer); } } diff --git a/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js b/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js index a1cbb0dcb2d5..eaa6ee9e4a65 100644 --- a/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js +++ b/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(e,t){"use strict";function s(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var i=s(e),n=s(t);var r=function(){const e={},t={};return{send(s,i){if(e[s])throw new Error("Name already provided.");e[s]=i;const n=t[s]||[];delete t[s],n.forEach((e=>e(i)))},onArrived(s,i){e[s]?i(e[s]):(t[s]=t[s]||[],t[s].push(i))}}}();const o=["padding-top","padding-bottom","padding-left","padding-right","margin-left","margin-right","margin-top","margin-bottom","width","font-size","font-family","font-style","font-weight","line-height","font-variant","text-transform","letter-spacing","border","box-sizing","display"],a=(e,t)=>{const s=window.getComputedStyle(e);o.forEach((e=>{t.style[e]=s[e]}))},l=e=>{let t="";return s=>{s!==t&&(e.style.height=s,t=s)}},c=e=>{const t=e.value;e.value="";const s=e.scrollHeight;e.value="\n";const i=e.scrollHeight-s;return e.value=t,i},h=(e,t)=>{const s=e.value;e.value="\n".repeat(t-1);const i=e.scrollHeight;return e.value=s,i},d=e=>{const t=document.createElement("textarea");return t.style.height=window.getComputedStyle(e).height,t.setAttribute("area-hidden","true"),t.readOnly=!0,t.disabled=!0,t},u=e=>{let t=()=>{const s=e();return t=()=>s,s};return()=>t()};function g(e,t,s){const i=d(t),n=l(t),r=u((()=>c(i))),o=u((()=>h(i,s))),g=()=>{i.value="";const e=i.scrollHeight,a=t.clientHeight;i.value=t.value;const l=i.scrollHeight,c=parseInt((l-e)/r()+1);l>e?n(c<=s?l+"px":o()+"px"):l{e.appendChild(i),a(t,i),g(),i.remove()}}var p=e=>(t,s={})=>{const i=new URL(e.replace(/postMessage/,t));return Object.entries(s).forEach((e=>m(i.searchParams,...e))),fetch(i)};function m(e,t,s){"object"==typeof s&&null!==s?Object.entries(s).forEach((([s,i])=>m(e,t+"["+s+"]",i))):e.set(t,s)}class f{#e;#t;constructor(e,t){this.#e=e,this.#t=t}heartbeatInterval(e){const t=()=>{};window.setInterval((()=>this.#s("poll",{},t)),e)}leavePrivateRoom(){this.#t.logILIASRequest("leavePrivateRoom"),this.#s("privateRoom-leave")}inviteToPrivateRoom(e,t){this.#s("inviteUsersToPrivateRoom-"+t,{user:e})}clear(){this.#s("clear")}kick(e){this.#s("kick",{user:e})}ban(e){this.#s("ban-active",{user:e})}#s(e,t={},s=(e=>this.#i(e))){this.#e(e,t).then((e=>e.json())).then(s)}#i(e){return this.#t.logILIASResponse("default"),!!e.success||(console.error(e.reason),!1)}}const v=()=>{},b=e=>new Promise((t=>r.onArrived(e,(({node:e,showModal:s,closeModal:i})=>{let n=v;e.querySelector("form").addEventListener("submit",(e=>(e.preventDefault(),n(!0),n=v,i(),!1))),t((()=>(s(),new Promise((e=>{n=e})))))}))));class y{#n;#r;#o;#a;#l;constructor(e,t){this.#n=e,this.#r=t,this.#o={},this.#a={},this.#l=()=>{}}imageOfUser(e){return this.#o[this.#c(e)]?Promise.resolve(this.#o[this.#c(e)]):this.#h(e)}imagesOfUsers(e){return Promise.all(e.map(this.imageOfUser.bind(this)))}defaultImage(){return this.#r}#h(e){return new Promise(((t,s)=>{const i=this.#c(e);this.#a[i]=this.#a[i]||{value:e,waiting:[]},this.#a[i].waiting.push({resolve:t,reject:s}),this.#l(),this.#l=clearTimeout.bind(null,setTimeout(this.#d.bind(this),20))}))}#d(){const e=Object.values(this.#a).map((({value:e})=>e)),t=fetch(this.#n,{method:"POST",body:JSON.stringify({profiles:e}),headers:{"Content-Type":"application/json"}}).then((e=>e.json()));t.then((e=>Object.entries(this.#u()).forEach((([t,{waiting:s}])=>s.forEach((({resolve:s,reject:i})=>{e[t]?(this.#o[t]=e[t],s(e[t])):i("Image not returned from server.")})))))),t.catch((e=>Object.values(this.#u()).flatMap((e=>e.waiting)).forEach((t=>t.reject(e)))))}#c(e){return JSON.stringify(e)}#u(){const e=this.#a;return this.#a={},e}}const k=(()=>{let e=0;return()=>(e++,"key-"+e)})(),S=e=>function(e){const t=k();return r.onArrived(t,e),t}((t=>t.addEventListener("click",e)));class C{#g;#e;#p;#m;#f;#v;#b;#y;constructor(e,t,s,i,n){this.#g=e,this.#e=t,this.#p=s,this.#m=i,this.#f=e&&e.querySelector(".no_users"),this.#v={},this.#b=[],this.#y=n}userListChanged(e){e.removed.forEach((({key:e})=>this.remove(e))),e.added.forEach((({value:e})=>this.add(e)))}add(e){if(this.#v[e.id])return!1;const t=this.#k(e);return this.#p(e.id)||this.#b.push(String(e.id)),this.#g.appendChild(t),this.#v[e.id]=t,this.#S(),!0}remove(e){const t=this.#v[e];return!!t&&(t.remove(),this.#b=this.#b.filter((t=>t!==e)),delete this.#v[e],this.#S(),!0)}setUsers(e){const t=e.map((e=>String(e.id)));Object.keys(this.#v).filter((e=>!t.includes(e))).forEach(this.remove.bind(this)),e.forEach(this.add.bind(this))}static actionList(e,t,s,i){return[{name:"kick",callback(e){s("kick-modal").then((s=>{s&&t.kick(e)}))}},{name:"ban",callback(e){s("ban-modal").then((s=>{s&&t.ban(e)}))}},{name:"chat",callback:i}]}#k(e){const t=document.createElement("div"),s=this.#e("view-userEntry",{username:e.username,user_id:e.id,actions:Object.fromEntries(this.#y.map((({name:t,callback:s})=>[t,S((()=>s(e.id)))])))}).then((e=>e.text())).then((e=>function(e,t){return e.innerHTML=t,Array.from(e.querySelectorAll("script"),(e=>{const t=document.createElement("script");t.appendChild(document.createTextNode(e.innerHTML)),e.parentNode.replaceChild(t,e)})),e}(t,e)));return t.classList.add("ilChatroomUser"),Promise.all([s,this.#m.imageOfUser(e)]).then((([e,s])=>{Array.from(t.querySelectorAll("img"),(e=>e.setAttribute("src",s)))})),this.#p(e.id)&&t.classList.add("ilNoDisplay"),t}#S(){this.#f.classList[this.#b.length?"add":"remove"]("ilNoDisplay")}}const L=(e,t)=>Object.keys(e).filter((e=>!Reflect.has(t,e))).map((t=>({key:t,value:e[t]})));class w{#C;#L;constructor(){this.#C={},this.#L=[]}find(e){return this.#C[e]}has(e){return Reflect.has(this.#C,String(e))}onChange(e){this.#L.push(e)}add(e,t){e=String(e),this.#C[e]=t,this.#w({added:[{key:e,value:t}],removed:[]})}remove(e){if(e=String(e),!Reflect.has(this.#C,e))return;const t=this.#C[e];delete this.#C[e],this.#w({added:[],removed:[{key:e,value:t}]})}setAll(e){const t={added:L(e,this.#C),removed:L(this.#C,e)};this.#C=e,this.#w(t)}all(){return this.#C}#w(e){this.#L.forEach((t=>t(e)))}}var I=(e,t)=>{const s=t instanceof Date?t:new Date(t),i=(n=2,e=>"0".repeat(Math.max(0,n-String(e).length))+e);var n;return[["Y",s.getFullYear()],["m",i(s.getMonth()+1)],["d",i(s.getDate())],["h",i(s.getHours()%12||12)],["H",i(s.getHours())],["i",i(s.getMinutes())],["s",i(s.getSeconds())],["a",s.getHours()>11?"pm":"am"]].reduce(((e,[t,s])=>e.replace(t,s)),e)};class U{#g;#I;#U;#m;#E;#_;#R;#T;#q;#A;#x;#M;constructor(e,t,s,i,n,r,o){this.#g=e,this.#I=t,this.#U=s,this.#m=i,this.#E=n,this.#_=r,this.#R=o,this.#A=_(["messageContainer"]),this.#A.setAttribute("aria-live","polite"),this.#x=_(["typing-info"]),this.#x.setAttribute("aria-live","polite"),this.#M=R,this.#O(),this.clearMessages(),this.#j()}addMessage(e){this.#M();const t=_(["messageLine","chat",!e.target||e.target.public?"public":"private"]),s=()=>console.warn("Unknown message type: ",e.type);let i=null;const n=e=>{i=e};({message:()=>{const s=function(e,t){const s=_(["message-body"]);return s.appendChild(e),s.appendChild(t),s}(function(e,t){const s=_(["time-info"]);return s.textContent=I(t.time,e.timestamp),s}(e,this.#R),function(e){const t=_([],"p");return t.textContent=e.content,E(t),t}(e));this.#q(new Date(e.timestamp))&&(this.#A.appendChild(function(e,t){const s=_(["separator"]),i=_([],"p");return i.textContent=I(t.date,e.timestamp),s.appendChild(i),s}(e,this.#R)),n(null)),e.from.id===this.#I&&t.classList.add("myself"),this.#T&&this.#T.id===e.from.id&&this.#T.username===e.from.username?(this.#T.node.appendChild(s),n(this.#T)):(t.appendChild(function(e,t,s){const i=_(["user"],"span"),n=_(["user"],"span"),r=_([],"img"),o=_(["message-header"]);return i.textContent=I(s.time,e.timestamp),n.textContent=e.from.username,r.src=t.defaultImage(),t.imageOfUser(e.from).then(Reflect.set.bind(null,r,"src")),o.appendChild(r),o.appendChild(n),o.appendChild(i),o}(e,this.#m,this.#R)),t.appendChild(s),this.#A.appendChild(t),n({...e.from,node:t}))},connected:s,disconnected:s,private_room_entered:s,private_room_left:s,notice:()=>{const t=_(["separator","system-message"]),s=_([],"p");s.textContent=this.#_(e.content,e.data),t.appendChild(s),this.#A.appendChild(t)},error:s,userjustkicked:s}[e.type]||s)(),this.#T=i,this.#U.scrolling&&(this.#g.scrollTop=this.#A.getBoundingClientRect().height)}clearMessages(){this.#A.textContent="",this.#T=null,this.#q=function(){let e=null;return t=>{const s=!e||e.getDate()!==t.getDate()||e.getMonth()!==t.getMonth()||e.getFullYear()!==t.getFullYear();return e=t,s}}();const e=_(["separator"]),t=_([],"p");t.textContent=this.#_("welcome_to_chat"),e.appendChild(t),this.#A.appendChild(e),this.#M=e.remove.bind(e)}typingListChanged(){const e=Object.values(this.#E.all());0===e.length?this.#x.textContent="":1===e.length?this.#x.textContent=this.#_("chat_user_x_is_typing",e[0]):this.#x.textContent=this.#_("chat_users_are_typing")}enableAutoScroll(e){this.#U.scrolling=Boolean(e),this.#O()}enableSystemMessages(e){this.#U.show_auto_msg=Boolean(e),this.#O()}#j(){this.#g.appendChild(this.#A);const e=_(["fader"]);this.#g.appendChild(e),e.appendChild(this.#x)}#O(){this.#g.classList[this.#U.show_auto_msg?"remove":"add"]("hide-system-messages")}}const E=(()=>{let e=t=>{try{i.default.ExtLink.autolink(t)}catch(t){console.error("Disabling url linking. Reason:",t),e=R}};return t=>e(t)})();function _(e,t){const s=document.createElement(t||"div");return(e||[]).forEach((e=>s.classList.add(e))),s}function R(){}class T{#P;#D;#H;#N;#E;#B;#t;#J;constructor(e,t,s,i,n,r,o){this.#P=e,this.#D=t,this.#H=s,this.#N=i,this.#E=n,this.#B=r,this.#t=o}init(e){this.#J=e,this.#J.on("message",this.#F.bind(this)),this.#J.on("connect",(()=>{this.#J.emit("login",this.#P.login,this.#P.id,this.#P.profile_picture_visible)})),this.#J.on("user_invited",this.#K.bind(this)),this.#J.on("private_room_entered",this.#Y.bind(this)),this.#J.on("connected",this.#Q.bind(this)),this.#J.on("userjustkicked",this.#z.bind(this)),this.#J.on("userjustbanned",this.#G.bind(this)),this.#J.on("clear",this.#V.bind(this)),this.#J.on("notice",this.#W.bind(this)),this.#J.on("userStartedTyping",this.#X.bind(this)),this.#J.on("userStoppedTyping",this.#Z.bind(this)),this.#J.on("userlist",this.#$.bind(this)),this.#J.on("shutdown",(()=>{this.#J.removeAllListeners(),this.#J.close(),window.location.href=this.#B})),window.addEventListener("beforeunload",(()=>{this.#J.close()}))}enterRoom(){this.#t.logServerRequest("enterRoom"),this.#J.emit("enterRoom",this.#N)}onLoggedIn(e){this.#J.on("loggedIn",e)}userStartedTyping(){this.#t.logServerRequest("userStartedTyping"),this.#J.emit("userStartedTyping",this.#N)}userStoppedTyping(){this.#t.logServerRequest("userStoppedTyping"),this.#J.emit("userStoppedTyping",this.#N)}sendMessage(e){this.#J.emit("message",e,this.#N)}#F(e){this.#H.addMessage(e)}#K(e){}#Y(e){this.#t.logServerResponse("onPrivateRoomEntered")}#Q(e){Object.values(e.users).forEach((t=>{let s={id:t.id,username:t.login,profile_picture_visible:t.profile_picture_visible};this.#D.add(s),this.#H.addMessage({login:s.label,timestamp:e.timestamp,type:"connected"})}))}#z(e){this.#t.logServerResponse("onUserKicked"),this.#D.remove(this.#P.id),window.location.href=this.#B+"&msg=kicked"}#G(e){this.#J&&(this.#J.removeAllListeners(),this.#J.close()),window.location.href=this.#B+"&msg=banned"}#V(){this.#H.clearMessages()}#W(e){this.#H.addMessage(e)}#X(e){this.#t.logServerResponse("onUserStartedTyping");const t=JSON.parse(e.subscriber);this.#E.add(t.id,t.username)}#Z(e){this.#t.logServerResponse("onUserStoppedTyping");const t=JSON.parse(e.subscriber);this.#E.remove(t.id)}#$(e){const t=e.users;this.#t.logServerResponse("onUserlist"),this.#D.setAll(Object.fromEntries(Object.values(t).map((e=>{const t={id:e.id,username:e.username,profile_picture_visible:e.profile_picture_visible};return[t.id,t]}))))}}class q{#ee;#te;#l;constructor(e){this.#ee=e,this.#te=!1,this.#l=()=>{},window.addEventListener("beforeunload",this.release.bind(this))}release(){this.#l(),this.#te&&(this.#ee.userStoppedTyping(),this.#te=!1)}heartbeat(){this.#l(),this.#te||(this.#ee.userStartedTyping(),this.#te=!0),this.#l=clearTimeout.bind(null,setTimeout(this.release.bind(this),5e3))}}class A{release(){}heartbeat(){}}var x=({closeModal:e,showModal:t,node:s},i,n,r,o)=>{const a=s.querySelector("input[type=text]");let l=null;s.querySelector("form").addEventListener("submit",(t=>(t.preventDefault(),null!=l&&(n.inviteToPrivateRoom(l,"byId"),e()),!1)));const c=function(e,t,s,i){const n=e.parentNode,r=document.createElement("div");r.classList.add("chat-autocomplete"),e.setAttribute("autocomplete","off"),n.appendChild(r);const o=t=>{i(t.id),e.value=t.value},a=function(e,t){let s=()=>{};return(...i)=>new Promise(((n,r)=>{s(),s=window.clearTimeout.bind(window,window.setTimeout((()=>e(...i).then(n).catch(r)),t))}))}((()=>function(e,t,s){if(s.length<3)return Promise.resolve([]);return t("inviteUsersToPrivateRoom-getUserList",{q:s}).then(M("json")).then((t=>t.items.filter((t=>!e.has(t.id)))))}(t,s,e.value).then(function(e,t){return s=>{e.innerHTML="",s.forEach((s=>{const i=document.createElement("button");i.textContent=s.label,e.appendChild(i),i.addEventListener("click",(()=>{e.innerHTML="",t(s)}))}))}}(r,o))),500);return e.addEventListener("input",(()=>{i(null),a()})),()=>{i(null),e.value="",r.innerHTML=""}}(a,r,o,(e=>{l=e}));return()=>{c(),t()}};const M=e=>t=>t[e]();class O{logServerResponse(e){this.#se("Server-Response",e)}logServerRequest(e){this.#se("Server-Request",e)}logILIASResponse(e){this.#se("ILIAS-Response",e)}logILIASRequest(e){this.#se("ILIAS-Request",e)}#se(e,t){console.log(e,t)}}const j=e=>{const t=new w,s=new w,o=new O,a=p(e.apiEndpointTemplate),l=((e,t=(e=>"#"+e+"#"))=>(s,i,...n)=>{let r=e[s];return r?(Object.entries(i||{}).forEach((([e,t])=>{r=r.split("#"+e+"#").join(t)})),r):t(s,i,...n)})(e.lang,i.default.Language.txt.bind(i.default.Language)),c=(()=>{const e=(e=>{const t={};return s=>(t[s]||(t[s]=e(s)),t[s])})(b);return t=>e(t).then((e=>e()))})(),h=new y(e.initial.profile_image_url,e.initial.no_profile_image_url),d=new f(a,o),u=new C(H("chat_users"),a,(t=>t===e.initial.userinfo.id),h,function(e,t){const s=t.userinfo.moderator?["kick","ban","chat"]:["chat"];return e.filter((e=>s.includes(e.name)))}(C.actionList(l,d,c,function(e){return t=>i.default.Chat.getConversation([i.default.OnScreenChat.user,e.find(t)])}(t)),e.initial)),m=new U(H("chat_messages"),e.initial.userinfo.id,e.initial.state,h,s,l,e.dateTimeFormatStrings),v=new T(e.initial.userinfo,t,m,e.scope,s,e.initial.redirect_url,o);return{bindEvents:function(){t.onChange(u.userListChanged.bind(u)),s.onChange(m.typingListChanged.bind(m)),D("auto-scroll-toggle",(e=>m.enableAutoScroll(e))),D("system-messages-toggle",(e=>m.enableSystemMessages(e))),D("system-messages-toggle",(t=>function(e,t){return fetch(t.system_message_update_url,{method:"POST",body:new URLSearchParams({state:Number(e)})})}(t,e.initial))),r.onArrived("invite-modal",(e=>P("invite-button",x(e,0,d,t,a)))),P("clear-history-button",(()=>function(e,t){e("clear-history-modal").then((e=>{e&&t.clear()}))}(c,d))),((e,t,s)=>{const i=e.querySelector("#submit_message_text");e.querySelector("#submit_message").addEventListener("click",(e=>{e.preventDefault(),e.stopPropagation(),r()}));const n=g(e.querySelector("#chat-shadow"),i,3);function r(){const e=i.value;if(""!==e.trim()){const r={content:e,format:{}};i.value="",s.release(),t(r),i.focus(),n()}}i.addEventListener("input",n),i.addEventListener("keydown",(e=>{13!==(e.keyCode||e.which)||e.shiftKey||(e.preventDefault(),e.stopPropagation(),i.blur(),r())})),i.addEventListener("keyup",(e=>{s[13===(e.keyCode||e.which)?"release":"heartbeat"]()}))})(H("send-message-group"),(e=>v.sendMessage(e)),e.initial.userinfo.broadcast_typing?new q(v):new A)},processInitialData:function(){(function(e,t){e.setAll(Object.fromEntries(t.users.map((e=>{const t={id:e.id,username:e.login,profile_picture_visible:e.profile_picture_visible};return[t.id,t]}))))})(t,e.initial),function(e,t){Object.values(t.messages).forEach((t=>{t.timestamp=1e3*t.timestamp,e.addMessage(t)}))}(m,e.initial)},connectToServer:function(){d.heartbeatInterval(12e4),v.init(n.default.connect(e.baseUrl+"/"+e.instance,{path:e.initial.subdirectory})),v.onLoggedIn((()=>{v.enterRoom(e.scope,0)}))}}};function P(e,t){r.onArrived(e,(e=>e.addEventListener("click",t)))}function D(e,t){P(e,(function(){t(this.classList.contains("on"))}))}function H(e){return document.getElementById(e)}i.default.Chatroom={run:e=>{const{bindEvents:t,processInitialData:s,connectToServer:i}=j(e);t(),s(),i(),H("submit_message_text").focus()},runReadOnly:e=>{const{processInitialData:t}=j(e);t()},bus:r,expandableTextarea:function(e,t,s){const i=e=>{const t=document.querySelector(e);return console.assert(null!==t,"Could not find selector "+JSON.stringify(e)),t};return g(i(e),i(t),s)}}}(il,io); +!function(e,t){"use strict";function s(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var i=s(e),n=s(t);function r(){const e={},t={};return{send(s,i){if(e[s])throw new Error("Name already provided.");e[s]=i;const n=t[s]||[];delete t[s],n.forEach((e=>e(i)))},onArrived(s,i){e[s]?i(e[s]):(t[s]=t[s]||[],t[s].push(i))}}}var o=r();const a=["padding-top","padding-bottom","padding-left","padding-right","margin-left","margin-right","margin-top","margin-bottom","width","font-size","font-family","font-style","font-weight","line-height","font-variant","text-transform","letter-spacing","border","box-sizing","display"],l=(e,t)=>{const s=window.getComputedStyle(e);a.forEach((e=>{t.style[e]=s[e]}))},c=e=>{let t="";return s=>{s!==t&&(e.style.height=s,t=s)}},h=e=>{const t=e.value;e.value="";const s=e.scrollHeight;e.value="\n";const i=e.scrollHeight-s;return e.value=t,i},d=(e,t)=>{const s=e.value;e.value="\n".repeat(t-1);const i=e.scrollHeight;return e.value=s,i},u=e=>{const t=document.createElement("textarea");return t.style.height=window.getComputedStyle(e).height,t.setAttribute("area-hidden","true"),t.readOnly=!0,t.disabled=!0,t},p=e=>{let t=()=>{const s=e();return t=()=>s,s};return()=>t()};function g(e,t,s){const i=u(t),n=c(t),r=p((()=>h(i))),o=p((()=>d(i,s))),a=()=>{i.value="";const e=i.scrollHeight,a=t.clientHeight;i.value=t.value;const l=i.scrollHeight,c=parseInt((l-e)/r()+1);l>e?n(c<=s?l+"px":o()+"px"):l{e.appendChild(i),l(t,i),a(),i.remove()}}var m=({closeModal:e,showModal:t,node:s},i,n,r,o)=>{const a=s.querySelector("input[type=text]");let l=null;s.querySelector("form").addEventListener("submit",(t=>(t.preventDefault(),null!=l&&(n(l),e()),!1)));const c=function(e,t,s,i,n){const r=e.parentNode,o=document.createElement("div"),a=document.createElement("ul"),l=document.createElement("div");o.classList.add("chat-autocomplete-container"),o.setAttribute("aria-live","asertive"),o.setAttribute("aria-relevant","additions"),o.setAttribute("role","status"),a.classList.add("chat-autocomplete"),a.classList.add("ilNoDisplay"),l.classList.add("ilNoDisplay"),l.appendChild(t.nothingFound),e.setAttribute("autocomplete","off"),o.appendChild(a),o.appendChild(l),r.appendChild(o);const c=t=>{n(t),e.value=t.value},h=n=>function(e,t,s){if(s.length<3)return Promise.resolve({items:[],hasMoreResults:!1,inputTooShort:!0});return t({search:s}).then((t=>({items:t.items.filter((t=>!e.includes(t.id))),hasMoreResults:t.hasMoreResults||!1})))}(s,n,e.value).then(function(e,t,s,i,n){return({items:r,hasMoreResults:o,inputTooShort:a})=>{const l=()=>{t.innerHTML="",t.classList.add("ilNoDisplay")},c=e=>()=>{l(),i(e)};if(0===r.length){if(!a)return l(),void s.classList.remove("ilNoDisplay");l()}else t.innerHTML="",t.classList.remove("ilNoDisplay");s.classList.add("ilNoDisplay"),r.forEach((s=>t.appendChild(function(e,t,s){const i=document.createElement("li"),n=document.createElement("button");return n.setAttribute("tabindex","0"),n.textContent=e.label,n.addEventListener("click",s),n.addEventListener("keydown",(e=>{({Enter:s,ArrowDown:()=>{(i.nextSibling||{querySelector:()=>t}).querySelector("button").focus()},ArrowUp:()=>{(i.previousSibling||{querySelector:()=>t}).querySelector("button").focus()}}[e.key]||f)()})),i.appendChild(n),i}(s,e,c(s))))),o&&t.appendChild(function(e,t){const s=document.createElement("li"),i=document.createElement("button");return i.classList.add("load-more"),i.textContent=e,s.appendChild(i),i.addEventListener("click",t),s}(n.label,(()=>{l(),n.load()})))}}(e,a,l,c,{load:()=>h((e=>i({...e,all:!0}))),label:t.more})),d=function(e,t){let s=f;return(...i)=>new Promise(((n,r)=>{s(),s=window.clearTimeout.bind(window,window.setTimeout((()=>e(...i).then(n).catch(r)),t))}))}((()=>h(i)),500);return e.addEventListener("input",(()=>{n(null),d()})),e.addEventListener("keydown",(e=>{"ArrowDown"===e.key&&a.firstChild?a.firstChild.querySelector("button").focus():"ArrowUp"===e.key&&a.lastChild&&a.lastChild.querySelector("button").focus()})),()=>{n(null),e.value="",a.innerHTML="",a.classList.add("ilNoDisplay"),l.classList.add("ilNoDisplay")}}(a,i,r,o,(e=>{l=e}));return()=>{c(),t()}};function f(){}var v=e=>(t,s={})=>{const i=new URL(e.replace(/postMessage/,t));return Object.entries(s).forEach((e=>y(i.searchParams,...e))),fetch(i)};function y(e,t,s){"object"==typeof s&&null!==s?Object.entries(s).forEach((([s,i])=>y(e,t+"["+s+"]",i))):e.set(t,s)}class b{#e;#t;constructor(e,t){this.#e=e,this.#t=t}heartbeatInterval(e){const t=()=>{};window.setInterval((()=>this.#s("poll",{},t)),e)}leavePrivateRoom(){this.#t.logILIASRequest("leavePrivateRoom"),this.#s("privateRoom-leave")}inviteToPrivateRoom(e,t){this.#s("inviteUsersToPrivateRoom-"+t,{user:e})}clear(){this.#s("clear")}kick(e){this.#s("kick",{user:e})}ban(e){this.#s("ban-active",{user:e})}#s(e,t={},s=(e=>this.#i(e))){this.#e(e,t).then((e=>e.json())).then(s)}#i(e){return this.#t.logILIASResponse("default"),!!e.success||(console.error(e.reason),!1)}}const L=()=>{},k=e=>new Promise((t=>o.onArrived(e,(({node:e,showModal:s,closeModal:i})=>{let n=L;e.querySelector("form").addEventListener("submit",(e=>(e.preventDefault(),n(!0),n=L,i(),!1))),t((()=>(s(),new Promise((e=>{n=e})))))}))));class C{#n;#r;#o;#a;#l;constructor(e,t){this.#n=e,this.#r=t,this.#o={},this.#a={},this.#l=()=>{}}imageOfUser(e){return this.#o[this.#c(e)]?Promise.resolve(this.#o[this.#c(e)]):this.#h(e)}imagesOfUsers(e){return Promise.all(e.map(this.imageOfUser.bind(this)))}defaultImage(){return this.#r}#h(e){return new Promise(((t,s)=>{const i=this.#c(e);this.#a[i]=this.#a[i]||{value:e,waiting:[]},this.#a[i].waiting.push({resolve:t,reject:s}),this.#l(),this.#l=clearTimeout.bind(null,setTimeout(this.#d.bind(this),20))}))}#d(){const e=Object.values(this.#a).map((({value:e})=>e)),t=fetch(this.#n,{method:"POST",body:JSON.stringify({profiles:e}),headers:{"Content-Type":"application/json"}}).then((e=>e.json()));t.then((e=>Object.entries(this.#u()).forEach((([t,{waiting:s}])=>s.forEach((({resolve:s,reject:i})=>{e[t]?(this.#o[t]=e[t],s(e[t])):i("Image not returned from server.")})))))),t.catch((e=>Object.values(this.#u()).flatMap((e=>e.waiting)).forEach((t=>t.reject(e)))))}#c(e){return JSON.stringify(e)}#u(){const e=this.#a;return this.#a={},e}}const S=(()=>{let e=0;return()=>(e++,"key-"+e)})(),w=e=>function(e){const t=S();return o.onArrived(t,e),t}((t=>t.addEventListener("click",e)));class E{#p;#e;#g;#m;#f;#v;#y;#b;constructor(e,t,s,i,n){this.#p=e,this.#e=t,this.#g=s,this.#m=i,this.#f=e&&e.querySelector(".no_users"),this.#v={},this.#y=[],this.#b=n}userListChanged(e){e.removed.forEach((({key:e})=>this.remove(e))),e.added.forEach((({value:e})=>this.add(e)))}add(e){if(this.#v[e.id])return!1;const t=this.#L(e);return this.#g(e.id)||this.#y.push(String(e.id)),this.#p.appendChild(t),this.#v[e.id]=t,this.#k(),!0}remove(e){const t=this.#v[e];return!!t&&(t.remove(),this.#y=this.#y.filter((t=>t!==e)),delete this.#v[e],this.#k(),!0)}setUsers(e){const t=e.map((e=>String(e.id)));Object.keys(this.#v).filter((e=>!t.includes(e))).forEach(this.remove.bind(this)),e.forEach(this.add.bind(this))}static actionList(e,t,s,i){return[{name:"kick",callback(e){s("kick-modal").then((s=>{s&&t.kick(e)}))}},{name:"ban",callback(e){s("ban-modal").then((s=>{s&&t.ban(e)}))}},{name:"chat",callback:i}]}#L(e){const t=document.createElement("div"),s=this.#e("view-userEntry",{username:e.username,user_id:e.id,actions:Object.fromEntries(this.#b.map((({name:t,callback:s})=>[t,w((()=>s(e.id)))])))}).then((e=>e.text())).then((e=>function(e,t){return e.innerHTML=t,Array.from(e.querySelectorAll("script"),(e=>{const t=document.createElement("script");t.appendChild(document.createTextNode(e.innerHTML)),e.parentNode.replaceChild(t,e)})),e}(t,e)));return t.classList.add("ilChatroomUser"),Promise.all([s,this.#m.imageOfUser(e)]).then((([e,s])=>{Array.from(t.querySelectorAll("img"),(e=>e.setAttribute("src",s)))})),this.#g(e.id)&&t.classList.add("ilNoDisplay"),t}#k(){this.#f.classList[this.#y.length?"add":"remove"]("ilNoDisplay")}}const U=(e,t)=>Object.keys(e).filter((e=>!Reflect.has(t,e))).map((t=>({key:t,value:e[t]})));class R{#C;#S;constructor(){this.#C={},this.#S=[]}find(e){return this.#C[e]}has(e){return Reflect.has(this.#C,String(e))}includes(e){return this.has(e)}onChange(e){this.#S.push(e)}add(e,t){e=String(e),this.#C[e]=t,this.#w({added:[{key:e,value:t}],removed:[]})}remove(e){if(e=String(e),!Reflect.has(this.#C,e))return;const t=this.#C[e];delete this.#C[e],this.#w({added:[],removed:[{key:e,value:t}]})}setAll(e){const t={added:U(e,this.#C),removed:U(this.#C,e)};this.#C=e,this.#w(t)}all(){return this.#C}#w(e){this.#S.forEach((t=>t(e)))}}var I=(e,t)=>{const s=t instanceof Date?t:new Date(t),i=(n=2,e=>"0".repeat(Math.max(0,n-String(e).length))+e);var n;return[["Y",s.getFullYear()],["m",i(s.getMonth()+1)],["d",i(s.getDate())],["h",i(s.getHours()%12||12)],["H",i(s.getHours())],["i",i(s.getMinutes())],["s",i(s.getSeconds())],["a",s.getHours()>11?"pm":"am"]].reduce(((e,[t,s])=>e.replace(t,s)),e)};class _{#p;#E;#U;#m;#R;#I;#_;#T;#A;#q;#x;#M;constructor(e,t,s,i,n,r,o){this.#p=e,this.#E=t,this.#U=s,this.#m=i,this.#R=n,this.#I=r,this.#_=o,this.#q=A(["messageContainer"]),this.#q.setAttribute("aria-live","polite"),this.#x=A(["typing-info"]),this.#x.setAttribute("aria-live","polite"),this.#M=q,this.#O(),this.clearMessages(),this.#j()}addMessage(e){this.#M();const t=A(["messageLine","chat",!e.target||e.target.public?"public":"private"]),s=()=>console.warn("Unknown message type: ",e.type);let i=null;const n=e=>{i=e};({message:()=>{const s=function(e,t){const s=A(["message-body"]);return s.appendChild(e),s.appendChild(t),s}(function(e,t){const s=A(["time-info"]);return s.textContent=I(t.time,e.timestamp),s}(e,this.#_),function(e){const t=A([],"p");return t.textContent=e.content,T(t),t}(e));this.#A(new Date(e.timestamp))&&(this.#q.appendChild(function(e,t){const s=A(["separator"]),i=A([],"p");return i.textContent=I(t.date,e.timestamp),s.appendChild(i),s}(e,this.#_)),n(null)),e.from.id===this.#E&&t.classList.add("myself"),this.#T&&this.#T.id===e.from.id&&this.#T.username===e.from.username?(this.#T.node.appendChild(s),n(this.#T)):(t.appendChild(function(e,t,s){const i=A(["user"],"span"),n=A(["user"],"span"),r=A([],"img"),o=A(["message-header"]);return i.textContent=I(s.time,e.timestamp),n.textContent=e.from.username,r.src=t.defaultImage(),t.imageOfUser(e.from).then(Reflect.set.bind(null,r,"src")),o.appendChild(r),o.appendChild(n),o.appendChild(i),o}(e,this.#m,this.#_)),t.appendChild(s),this.#q.appendChild(t),n({...e.from,node:t}))},connected:s,disconnected:s,private_room_entered:s,private_room_left:s,notice:()=>{const t=A(["separator","system-message"]),s=A([],"p");s.textContent=this.#I(e.content,e.data),t.appendChild(s),this.#q.appendChild(t)},error:s,userjustkicked:s}[e.type]||s)(),this.#T=i,this.#U.scrolling&&(this.#p.scrollTop=this.#q.getBoundingClientRect().height)}clearMessages(){this.#q.textContent="",this.#T=null,this.#A=function(){let e=null;return t=>{const s=!e||e.getDate()!==t.getDate()||e.getMonth()!==t.getMonth()||e.getFullYear()!==t.getFullYear();return e=t,s}}();const e=A(["separator"]),t=A([],"p");t.textContent=this.#I("welcome_to_chat"),e.appendChild(t),this.#q.appendChild(e),this.#M=e.remove.bind(e)}typingListChanged(){const e=Object.values(this.#R.all());0===e.length?this.#x.textContent="":1===e.length?this.#x.textContent=this.#I("chat_user_x_is_typing",e[0]):this.#x.textContent=this.#I("chat_users_are_typing")}enableAutoScroll(e){this.#U.scrolling=Boolean(e),this.#O()}enableSystemMessages(e){this.#U.show_auto_msg=Boolean(e),this.#O()}#j(){this.#p.appendChild(this.#q);const e=A(["fader"]);this.#p.appendChild(e),e.appendChild(this.#x)}#O(){this.#p.classList[this.#U.show_auto_msg?"remove":"add"]("hide-system-messages")}}const T=(()=>{let e=t=>{try{i.default.ExtLink.autolink(t)}catch(t){console.error("Disabling url linking. Reason:",t),e=q}};return t=>e(t)})();function A(e,t){const s=document.createElement(t||"div");return(e||[]).forEach((e=>s.classList.add(e))),s}function q(){}class x{#D;#P;#N;#H;#R;#F;#t;#B;constructor(e,t,s,i,n,r,o){this.#D=e,this.#P=t,this.#N=s,this.#H=i,this.#R=n,this.#F=r,this.#t=o}init(e){this.#B=e,this.#B.on("message",this.#J.bind(this)),this.#B.on("connect",(()=>{this.#B.emit("login",this.#D.login,this.#D.id,this.#D.profile_picture_visible)})),this.#B.on("user_invited",this.#K.bind(this)),this.#B.on("private_room_entered",this.#Y.bind(this)),this.#B.on("connected",this.#Q.bind(this)),this.#B.on("userjustkicked",this.#z.bind(this)),this.#B.on("userjustbanned",this.#G.bind(this)),this.#B.on("clear",this.#V.bind(this)),this.#B.on("notice",this.#W.bind(this)),this.#B.on("userStartedTyping",this.#X.bind(this)),this.#B.on("userStoppedTyping",this.#Z.bind(this)),this.#B.on("userlist",this.#$.bind(this)),this.#B.on("shutdown",(()=>{this.#B.removeAllListeners(),this.#B.close(),window.location.href=this.#F})),window.addEventListener("beforeunload",(()=>{this.#B.close()}))}enterRoom(){this.#t.logServerRequest("enterRoom"),this.#B.emit("enterRoom",this.#H)}onLoggedIn(e){this.#B.on("loggedIn",e)}userStartedTyping(){this.#t.logServerRequest("userStartedTyping"),this.#B.emit("userStartedTyping",this.#H)}userStoppedTyping(){this.#t.logServerRequest("userStoppedTyping"),this.#B.emit("userStoppedTyping",this.#H)}sendMessage(e){this.#B.emit("message",e,this.#H)}#J(e){this.#N.addMessage(e)}#K(e){}#Y(e){this.#t.logServerResponse("onPrivateRoomEntered")}#Q(e){Object.values(e.users).forEach((t=>{let s={id:t.id,username:t.login,profile_picture_visible:t.profile_picture_visible};this.#P.add(s),this.#N.addMessage({login:s.label,timestamp:e.timestamp,type:"connected"})}))}#z(e){this.#t.logServerResponse("onUserKicked"),this.#P.remove(this.#D.id),window.location.href=this.#F+"&msg=kicked"}#G(e){this.#B&&(this.#B.removeAllListeners(),this.#B.close()),window.location.href=this.#F+"&msg=banned"}#V(){this.#N.clearMessages()}#W(e){this.#N.addMessage(e)}#X(e){this.#t.logServerResponse("onUserStartedTyping");const t=JSON.parse(e.subscriber);this.#R.add(t.id,t.username)}#Z(e){this.#t.logServerResponse("onUserStoppedTyping");const t=JSON.parse(e.subscriber);this.#R.remove(t.id)}#$(e){const t=e.users;this.#t.logServerResponse("onUserlist"),this.#P.setAll(Object.fromEntries(Object.values(t).map((e=>{const t={id:e.id,username:e.username,profile_picture_visible:e.profile_picture_visible};return[t.id,t]}))))}}class M{#ee;#te;#l;constructor(e){this.#ee=e,this.#te=!1,this.#l=()=>{},window.addEventListener("beforeunload",this.release.bind(this))}release(){this.#l(),this.#te&&(this.#ee.userStoppedTyping(),this.#te=!1)}heartbeat(){this.#l(),this.#te||(this.#ee.userStartedTyping(),this.#te=!0),this.#l=clearTimeout.bind(null,setTimeout(this.release.bind(this),5e3))}}class O{release(){}heartbeat(){}}class j{logServerResponse(e){this.#se("Server-Response",e)}logServerRequest(e){this.#se("Server-Request",e)}logILIASResponse(e){this.#se("ILIAS-Response",e)}logILIASRequest(e){this.#se("ILIAS-Request",e)}#se(e,t){console.log(e,t)}}const D=e=>{const t=new R,s=new R,r=new j,a=v(e.apiEndpointTemplate),l=((e,t=(e=>"#"+e+"#"))=>(s,i,...n)=>{let r=e[s];return r?(Object.entries(i||{}).forEach((([e,t])=>{r=r.split("#"+e+"#").join(t)})),r):t(s,i,...n)})(e.lang,i.default.Language.txt.bind(i.default.Language)),c=(()=>{const e=(e=>{const t={};return s=>(t[s]||(t[s]=e(s)),t[s])})(k);return t=>e(t).then((e=>e()))})(),h=new C(e.initial.profile_image_url,e.initial.no_profile_image_url),d=new b(a,r),u=new E(H("chat_users"),a,(t=>t===e.initial.userinfo.id),h,function(e,t){const s=t.userinfo.moderator?["kick","ban","chat"]:["chat"];return e.filter((e=>s.includes(e.name)))}(E.actionList(l,d,c,function(e){return t=>i.default.Chat.getConversation([i.default.OnScreenChat.user,e.find(t)])}(t)),e.initial)),p=new _(H("chat_messages"),e.initial.userinfo.id,e.initial.state,h,s,l,e.dateTimeFormatStrings),f=new x(e.initial.userinfo,t,p,e.scope,s,e.initial.redirect_url,r);return{bindEvents:function(){t.onChange(u.userListChanged.bind(u)),s.onChange(p.typingListChanged.bind(p)),N("auto-scroll-toggle",(e=>p.enableAutoScroll(e))),N("system-messages-toggle",(e=>p.enableSystemMessages(e))),N("system-messages-toggle",(t=>function(e,t){return fetch(t.system_message_update_url,{method:"POST",body:new URLSearchParams({state:Number(e)})})}(t,e.initial))),o.onArrived("invite-modal",(s=>P("invite-button",m(s,{more:"»"+l("autocomplete_more"),nothingFound:e.nothingFound},(e=>d.inviteToPrivateRoom(e.id,"byId")),t,(({search:e,all:t})=>a("inviteUsersToPrivateRoom-getUserList",Object.assign({q:e},t?{fetchall:"1"}:{})).then((e=>e.json()))))))),P("clear-history-button",(()=>function(e,t){e("clear-history-modal").then((e=>{e&&t.clear()}))}(c,d))),((e,t,s)=>{const i=e.querySelector("#submit_message_text");e.querySelector("#submit_message").addEventListener("click",(e=>{e.preventDefault(),e.stopPropagation(),r()}));const n=g(e.querySelector("#chat-shadow"),i,3);function r(){const e=i.value;if(""!==e.trim()){const r={content:e,format:{}};i.value="",s.release(),t(r),i.focus(),n()}}i.addEventListener("input",n),i.addEventListener("keydown",(e=>{13!==(e.keyCode||e.which)||e.shiftKey||(e.preventDefault(),e.stopPropagation(),i.blur(),r())})),i.addEventListener("keyup",(e=>{s[13===(e.keyCode||e.which)?"release":"heartbeat"]()}))})(H("send-message-group"),(e=>f.sendMessage(e)),e.initial.userinfo.broadcast_typing?new M(f):new O)},processInitialData:function(){(function(e,t){e.setAll(Object.fromEntries(t.users.map((e=>{const t={id:e.id,username:e.login,profile_picture_visible:e.profile_picture_visible};return[t.id,t]}))))})(t,e.initial),function(e,t){Object.values(t.messages).forEach((t=>{t.timestamp=1e3*t.timestamp,e.addMessage(t)}))}(p,e.initial)},connectToServer:function(){d.heartbeatInterval(12e4),f.init(n.default.connect(e.baseUrl+"/"+e.instance,{path:e.initial.subdirectory})),f.onLoggedIn((()=>{f.enterRoom(e.scope,0)}))}}};function P(e,t){o.onArrived(e,(e=>e.addEventListener("click",t)))}function N(e,t){P(e,(function(){t(this.classList.contains("on"))}))}function H(e){return document.getElementById(e)}i.default.Chatroom={run:e=>{const{bindEvents:t,processInitialData:s,connectToServer:i}=D(e);t(),s(),i(),H("submit_message_text").focus()},runReadOnly:e=>{const{processInitialData:t}=D(e);t()},createBus:r,bus:o,expandableTextarea:function(e,t,s){const i=e=>{const t=document.querySelector(e);return console.assert(null!==t,"Could not find selector "+JSON.stringify(e)),t};return g(i(e),i(t),s)},inviteUserToRoom:m,sendFromURL:v}}(il,io); diff --git a/components/ILIAS/Chatroom/resources/js/src/WatchList.js b/components/ILIAS/Chatroom/resources/js/src/WatchList.js index 6f4954c75b7f..69e7efcbc5c3 100644 --- a/components/ILIAS/Chatroom/resources/js/src/WatchList.js +++ b/components/ILIAS/Chatroom/resources/js/src/WatchList.js @@ -37,6 +37,10 @@ export default class WatchList { return Reflect.has(this.#list, String(key)); } + includes(key) { + return this.has(key); + } + onChange(callback) { this.#onChangeList.push(callback); } diff --git a/components/ILIAS/Chatroom/resources/js/src/index.js b/components/ILIAS/Chatroom/resources/js/src/index.js index d949aa508ab0..6a8b370b70f7 100644 --- a/components/ILIAS/Chatroom/resources/js/src/index.js +++ b/components/ILIAS/Chatroom/resources/js/src/index.js @@ -15,13 +15,18 @@ *********************************************************************/ import il from 'il'; -import bus from './bus'; +import bus, { createBus } from './bus'; import { expandableTextarea } from './expandableTextarea'; +import inviteUserToRoom from './inviteUserToRoom'; +import sendFromURL from './sendFromURL'; import run, { runReadOnly } from './run'; il.Chatroom = { run, runReadOnly, + createBus, bus, expandableTextarea, + inviteUserToRoom, + sendFromURL, }; diff --git a/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js b/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js index 4891df4e9c84..d4cdd686fcc2 100644 --- a/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js +++ b/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js @@ -14,20 +14,20 @@ * *********************************************************************/ -export default ({closeModal, showModal, node}, translation, iliasConnector, userList, send) => { +export default ({closeModal, showModal, node}, labels, invite, userList, send) => { const input = node.querySelector('input[type=text]'); let value = null; node.querySelector('form').addEventListener('submit', e => { e.preventDefault(); if (value != null) { - iliasConnector.inviteToPrivateRoom(value, 'byId'); + invite(value); closeModal(); } return false; }); - const reset = autocomplete(input, userList, send, v => { + const reset = autocomplete(input, labels, userList, send, v => { value = v; }); @@ -37,52 +37,136 @@ export default ({closeModal, showModal, node}, translation, iliasConnector, user }; }; -function autocomplete(node, userList, send, setId) { - const p = node.parentNode; - const list = document.createElement('div'); +function autocomplete(input, labels, userList, send, setValue) { + const parent = input.parentNode; + const container = document.createElement('div'); + const list = document.createElement('ul'); + const nothingFound = document.createElement('div'); + container.classList.add('chat-autocomplete-container'); + container.setAttribute('aria-live', 'asertive'); + container.setAttribute('aria-relevant', 'additions'); + container.setAttribute('role', 'status'); list.classList.add('chat-autocomplete'); - node.setAttribute('autocomplete', 'off'); - p.appendChild(list); + list.classList.add('ilNoDisplay'); + nothingFound.classList.add('ilNoDisplay'); + nothingFound.appendChild(labels.nothingFound); + input.setAttribute('autocomplete', 'off'); + container.appendChild(list); + container.appendChild(nothingFound); + parent.appendChild(container); - const set = entry => { - setId(entry.id); - node.value = entry.value; + const select = entry => { + setValue(entry); + input.value = entry.value; }; - const search = debounce( - () => searchForUsers(userList, send, node.value).then(displayResults(list, set)), - 500 - ); + const search = sendSearch => searchForUsers(userList, sendSearch, input.value).then(displayResults(input, list, nothingFound, select, { + load: () => search(s => send({...s, all: true})), + label: labels.more, + })); + + const searchDelayed = debounce(() => search(send), 500); - node.addEventListener('input', () => { - setId(null); - search(); + input.addEventListener('input', () => { + setValue(null); + searchDelayed(); }); + input.addEventListener('keydown', e => { + if (e.key === 'ArrowDown' && list.firstChild) { + list.firstChild.querySelector('button').focus(); + } else if (e.key === 'ArrowUp' && list.lastChild) { + list.lastChild.querySelector('button').focus(); + } + }) + return () => { - setId(null); - node.value = ''; + setValue(null); + input.value = ''; list.innerHTML = ''; + list.classList.add('ilNoDisplay'); + nothingFound.classList.add('ilNoDisplay'); }; } -function displayResults(node, set) { - return results => { - node.innerHTML = ''; - results.forEach(entry => { - const b = document.createElement('button'); - b.textContent = entry.label; - node.appendChild(b); - b.addEventListener('click', () => { - node.innerHTML = ''; - set(entry); - }); - }); +function displayResults(input, list, nothingFound, select, more) { + return ({items: results, hasMoreResults, inputTooShort}) => { + const clearList = () => { + list.innerHTML = ''; + list.classList.add('ilNoDisplay'); + }; + const willSelect = entry => () => { + clearList(); + select(entry); + }; + if (results.length === 0) { + if (inputTooShort) { + clearList(); + } else { + clearList(); + nothingFound.classList.remove('ilNoDisplay'); + return; + } + } else { + list.innerHTML = ''; + list.classList.remove('ilNoDisplay'); + } + + nothingFound.classList.add('ilNoDisplay'); + results.forEach(entry => list.appendChild(createResultItem(entry, input, willSelect(entry)))); + + if (hasMoreResults) { + list.appendChild(createLoadMoreItem(more.label, () => { + clearList(); + more.load(); + })); + } }; } +function createResultItem(entry, input, select) +{ + const li = document.createElement('li'); + const button = document.createElement('button'); + + button.setAttribute('tabindex', '0'); + button.textContent = entry.label; + button.addEventListener('click', select); + button.addEventListener('keydown', e => { + const cases = { + 'Enter': select, + 'ArrowDown': () => { + (li.nextSibling || {querySelector: () => input}).querySelector('button').focus(); + }, + 'ArrowUp': () => { + (li.previousSibling || {querySelector: () => input}).querySelector('button').focus(); + }, + }; + + (cases[e.key] || Void)(); + }); + + li.appendChild(button); + + return li; +} + +function createLoadMoreItem(label, loadMore) +{ + const li = document.createElement('li'); + const button = document.createElement('button'); + button.classList.add('load-more'); + button.textContent = label; + + li.appendChild(button); + + button.addEventListener('click', loadMore); + + return li; +} + function debounce(proc, delay) { - let del = () => {}; + let del = Void; return (...args) => { return new Promise((ok, err) => { del(); @@ -94,15 +178,16 @@ function debounce(proc, delay) { }; } -const call = m => o => o[m](); - function searchForUsers(userList, send, search) { if (search.length < 3) { - return Promise.resolve([]); + return Promise.resolve({items: [], hasMoreResults: false, inputTooShort: true}); } - return send('inviteUsersToPrivateRoom-getUserList', {q: search}).then(call('json')).then( - response => { - return response.items.filter(item => !userList.has(item.id)); - } + return send({search}).then( + response => ({ + items: response.items.filter(item => !userList.includes(item.id)), + hasMoreResults: response.hasMoreResults || false, + }) ); } + +function Void() {} diff --git a/components/ILIAS/Chatroom/resources/js/src/run.js b/components/ILIAS/Chatroom/resources/js/src/run.js index 2c8ec02b949f..993f2972766c 100644 --- a/components/ILIAS/Chatroom/resources/js/src/run.js +++ b/components/ILIAS/Chatroom/resources/js/src/run.js @@ -86,10 +86,13 @@ const setup = options => { toggle('system-messages-toggle', on => saveShowSystemMessageState(on, options.initial)); bus.onArrived('invite-modal', modalData => click('invite-button', inviteUserToRoom( modalData, - txt, - iliasConnector, + { + more: '»' + txt('autocomplete_more'), + nothingFound: options.nothingFound, + }, + value => iliasConnector.inviteToPrivateRoom(value.id, 'byId'), userList, - send + ({search, all}) => send('inviteUsersToPrivateRoom-getUserList', Object.assign({q: search}, all ? {fetchall: '1'} : {})).then(r => r.json()) ))); click('clear-history-button', () => clearHistory(confirmModal, iliasConnector)); diff --git a/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html b/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html index 5f3c0002013f..ac6255057364 100755 --- a/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html +++ b/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html @@ -40,6 +40,7 @@ initial: {INITIAL_DATA}, apiEndpointTemplate: {POSTURL}, dateTimeFormatStrings: {date: {DATE_FORMAT}, time: {TIME_FORMAT}}, + nothingFound: new DOMParser().parseFromString({NOTHING_FOUND}, 'text/html').body.firstChild, lang, }; {JS_CALL}(options); diff --git a/components/ILIAS/Chatroom/tests/ilChatroomUserTest.php b/components/ILIAS/Chatroom/tests/ilChatroomUserTest.php index b3e7590f5813..ca9815a5546f 100644 --- a/components/ILIAS/Chatroom/tests/ilChatroomUserTest.php +++ b/components/ILIAS/Chatroom/tests/ilChatroomUserTest.php @@ -109,7 +109,7 @@ public function testGetUsernameFromIlObjUser(): void ], ]); - $this->ilUserMock->expects($this->once())->method('getLogin')->willReturn($username); + $this->ilUserMock->expects($this->once())->method('getPublicName')->willReturn($username); $this->ilChatroomMock->method('getRoomId')->willReturn($roomId); $this->assertSame($username, $this->user->getUsername()); diff --git a/components/ILIAS/OnScreenChat/classes/class.ilOnScreenChatGUI.php b/components/ILIAS/OnScreenChat/classes/class.ilOnScreenChatGUI.php index c9f1153f30a3..9ae955f3a1b2 100755 --- a/components/ILIAS/OnScreenChat/classes/class.ilOnScreenChatGUI.php +++ b/components/ILIAS/OnScreenChat/classes/class.ilOnScreenChatGUI.php @@ -95,6 +95,26 @@ public function executeCommand(): void ); break; + case 'inviteModal': + $this->dic->language()->loadLanguageModule('chatroom'); + $txt = $this->dic->language()->txt(...); + $modal = $this->dic->ui()->factory()->modal()->roundtrip($txt('chat_osc_invite_to_conversation'), $this->dic->ui()->factory()->legacy($txt('chat_osc_search_modal_info')), [ + $this->dic->ui()->factory()->input()->field()->text($txt('chat_osc_user')), + ])->withSubmitLabel($txt('confirm')); + $response = $this->renderAsyncModal('inviteModal', $modal); + break; + + case 'confirmRemove': + $this->dic->language()->loadLanguageModule('chatroom'); + $txt = $this->dic->language()->txt(...); + $modal = $this->dic->ui()->factory()->modal()->interruptive( + $txt('chat_osc_leave_grp_conv'), + $txt('chat_osc_sure_to_leave_grp_conv'), + '' + )->withActionButtonLabel($txt('confirm')); + $response = $this->renderAsyncModal('confirmRemove', $modal); + break; + case 'getUserlist': default: $response = $this->getUserList(); @@ -198,15 +218,16 @@ public static function initializeFrontend(ilGlobalTemplateInterface $page): void false, 'components/ILIAS/OnScreenChat' ))->get(), - 'modalTemplate' => (new ilTemplate( - 'tpl.chat-add-user.html', - false, - false, - 'components/ILIAS/OnScreenChat' - ))->get(), + 'nothingFoundTemplate' => $DIC->ui()->renderer()->render($DIC->ui()->factory()->messageBox()->info($DIC->language()->txt('chat_osc_no_usr_found'))), 'userId' => $DIC->user()->getId(), 'username' => $DIC->user()->getLogin(), - 'userListURL' => $DIC->ctrl()->getLinkTargetByClass( + 'modalURLTemplate' => ILIAS_HTTP_PATH . '/' . $DIC->ctrl()->getLinkTargetByClass( + ilOnScreenChatGUI::class, + 'postMessage', + null, + true + ), + 'userListURL' => ILIAS_HTTP_PATH . '/' . $DIC->ctrl()->getLinkTargetByClass( 'ilonscreenchatgui', 'getUserList', '', @@ -288,6 +309,9 @@ public static function initializeFrontend(ilGlobalTemplateInterface $page): void iljQueryUtil::initjQueryUI($page); ilLinkifyUtil::initLinkify($page); + $page->addJavaScript('assets/js/modal.js'); + $page->addJavaScript('assets/js/socket.io.min.js'); + $page->addJavaScript('assets/js/Chatroom.min.js'); $page->addJavaScript('assets/js/jquery.ui.touch-punch.js'); $page->addJavascript('assets/js/LegacyModal.js'); $page->addJavascript('assets/js/moment-with-locales.min.js'); @@ -311,4 +335,11 @@ public static function initializeFrontend(ilGlobalTemplateInterface $page): void self::$frontend_initialized = true; } } + + private function renderAsyncModal(string $bus_name, $modal) + { + return $this->getResponseWithText($this->dic->ui()->renderer()->renderAsync($modal->withAdditionalOnLoadCode(fn ($id) => ( + 'il.OnScreenChat.bus.send(' . json_encode($bus_name, JSON_THROW_ON_ERROR) . ', ' . json_encode([(string) $modal->getShowSignal(), (string) $modal->getCloseSignal()], JSON_THROW_ON_ERROR) . ');' + )))); + } } diff --git a/components/ILIAS/OnScreenChat/resources/onscreenchat.js b/components/ILIAS/OnScreenChat/resources/onscreenchat.js index 4338d40ff2ac..fc6534f28d7c 100644 --- a/components/ILIAS/OnScreenChat/resources/onscreenchat.js +++ b/components/ILIAS/OnScreenChat/resources/onscreenchat.js @@ -27,27 +27,18 @@ const resizeTextareas = {}; // string: function const MAX_CHAT_LINES = 3; - $.widget("custom.iloscautocomplete", $.ui.autocomplete, { - more: false, - _renderMenu: function(ul, items) { - var that = this; - $.each(items, function(index, item) { - that._renderItemData(ul, item); - }); - - that.options.requestUrl = that.options.requestUrl.replace(/&fetchall=1/g, ''); - - if (that.more) { - ul.append("
  • »" + il.Language.txt("autocomplete_more") + "
  • "); - ul.find('li').last().on('click', function(e) { - that.options.requestUrl += '&fetchall=1'; - that.close(e); - that.search(null, e); - e.preventDefault(); - }); + const tryLink = (() => { + let link = node => { + try { + il.ExtLink.autolink(node); + } catch (error) { + console.error('Disabling url linking. Reason:', error); + link = () => {}; } - } - }); + }; + + return node => link(node); + })(); const triggerMap = { participantEvent: ['click', '[data-onscreenchat-userid]'], @@ -103,6 +94,7 @@ conversationItems: {}, conversationMessageTimes: {}, conversationToUiIdMap: {}, + bus: il.Chatroom.createBus(), setConversationMessageTimes: function(timeInfo) { getModule().conversationMessageTimes = timeInfo; @@ -120,6 +112,56 @@ init: function() { getModule().storage = new ConversationStorage(); + const loadModal = busName => il.Chatroom.sendFromURL(getModule().config.modalURLTemplate)(busName).then(r => r.text()).then(modalHTML => { + const modal = $(modalHTML); + $(document.body).append(modal); + return new Promise(resolve => getModule().bus.onArrived(busName, ([showSignal, closeSignal]) => resolve({ + showModal: () => $(document).trigger(showSignal, {}), + closeModal: () => $(document).trigger(closeSignal, {}), + node: modal[0], + }))) + }); + + const confirmModal = lazy(() => { + let currentThen = null; + const modal = loadModal('confirmRemove'); + modal.then(({node, closeModal}) => node.querySelector('form').addEventListener('submit', e => { + e.preventDefault(); + closeModal(); + currentThen(); + })); + + return then => { + currentThen = then; + modal.then(({showModal}) => showModal()); + } + }); + + const inviteModal = lazy(() => { + let currentConversationId = null; + const setup = loadModal('inviteModal').then(modalInfo => il.Chatroom.inviteUserToRoom( + modalInfo, + { + more: '»' + il.Language.txt('autocomplete_more'), + nothingFound: $(getModule().config.nothingFoundTemplate)[0], + }, + entry => getModule().addUser(currentConversationId, entry.id, entry.value), + ((getModule().storage.get(currentConversationId) || {}).participants || []).map(p => p.id), + ({search, all}) => il.Chatroom.sendFromURL(getModule().config.userListURL)( + '', + Object.assign({term: search}, all ? {fetchall: '1'} : {}) + ).then(r => r.json()) + )); + + return conversationId => { + currentConversationId = conversationId; + setup.then(open => open()); + }; + }); + + getModule().openConfirmModal = then => confirmModal()(then); + getModule().openInviteUserModal = conversationId => inviteModal()(conversationId); + $.each(getModule().config.initialUserData, function(usrId, item) { getModule().participantsNames[usrId] = item.public_name; @@ -732,35 +774,9 @@ let conversation = getModule().storage.get(conversationId); if (conversation.isGroup) { - $scope.il.Modal.dialogue({ - id: 'modal-leave-' + conversation.id, - header: il.Language.txt('chat_osc_leave_grp_conv'), - body: il.Language.txt('chat_osc_sure_to_leave_grp_conv'), - buttons: { - confirm: { - type: "button", - label: il.Language.txt("confirm"), - - className: "btn btn-primary", - callback: function (e, modal) { - e.stopPropagation(); - modal.modal("hide"); - - $chat.closeConversation(conversationId, getModule().user.id); - $chat.removeUser(conversationId, getModule().user.id, getModule().user.name); - } - }, - cancel: { - label: il.Language.txt("cancel"), - type: "button", - className: "btn btn-default", - callback: function (e, modal) { - e.stopPropagation(); - modal.modal("hide"); - } - } - }, - show: true + getModule().openConfirmModal(() => { + $chat.closeConversation(conversationId, getModule().user.id); + $chat.removeUser(conversationId, getModule().user.id, getModule().user.name); }); } else { $chat.closeConversation(conversationId, getModule().user.id); @@ -885,82 +901,7 @@ e.preventDefault(); e.stopPropagation(); - $scope.il.Modal.dialogue({ - id: 'modal-' + $(this).attr('data-onscreenchat-add'), - header: il.Language.txt('chat_osc_invite_to_conversation'), - show: true, - body: getModule().config.modalTemplate - .replace(/\[\[conversationId\]\]/g, $(this).attr('data-onscreenchat-add')) - .replace('#:#chat_osc_search_modal_info#:#', il.Language.txt('chat_osc_search_modal_info')) - .replace('#:#chat_osc_user#:#', il.Language.txt('chat_osc_user')) - .replace('#:#chat_osc_no_usr_found#:#', il.Language.txt('chat_osc_no_usr_found')), - onShown: function (e, modal) { - var modalBody = modal.find('[data-onscreenchat-modal-body]'), - conversation = getModule().storage.get(modalBody.data('onscreenchat-modal-body')), - $elm = modal.find('input[type="text"]').first(); - - modal.find("form").on("keyup keydown keypress", function(fe) { - if (fe.which == 13) { - if ( - $(fe.target).prop("tagName").toLowerCase() != "textarea" && - ( - $(fe.target).prop("tagName").toLowerCase() != "input" || - $(fe.target).prop("type") != "submit" - )) { - fe.preventDefault(); - } - } - }); - - $elm.focus().iloscautocomplete({ - appendTo: $elm.parent(), - requestUrl: getModule().config.userListURL, - source: function(request, response) { - var that = this; - $.getJSON(that.options.requestUrl, { - term: request.term - }, function(data) { - if (typeof data.items === "undefined") { - if (data.length === 0) { - modalBody.find('[data-onscreenchat-no-usr-found]').removeClass("ilNoDisplay"); - } - response(data); - } else { - that.more = data.hasMoreResults; - if (data.items.length === 0) { - modalBody.find('[data-onscreenchat-no-usr-found]').removeClass("ilNoDisplay"); - } - response(data.items); - } - }); - }, - search: function() { - var term = this.value; - - if (term.length < 3) { - return false; - } - - modalBody.find('label').append( - $('').addClass("ilOnScreenChatSearchLoader").attr("src", getConfig().loaderImg) - ); - modalBody.find('[data-onscreenchat-no-usr-found]').addClass("ilNoDisplay"); - }, - response: function() { - $(".ilOnScreenChatSearchLoader").remove(); - }, - select: function(event, ui) { - var userId = ui.item.id, - name = ui.item.value; - - if (userId > 0) { - getModule().addUser(conversation.id, userId, name); - $scope.il.Modal.dialogue({id: "modal-" + conversation.id}).hide(); - } - } - }); - } - }); + getModule().openInviteUserModal(this.getAttribute('data-onscreenchat-add')); }, trackActivityFor: function(conversation){ @@ -1044,7 +985,7 @@ let messageDate = new Date(); messageDate.setTime(messageObject.timestamp); const placeholderClass = 'm' + new Date().getTime(); - const placeholder = ''; + const placeholder = ''; template = template.replace(/\[\[username\]\]/g, findUsernameInConversationByMessage(messageObject)); template = template.replace(/\[\[time_raw\]\]/g, messageObject.timestamp); @@ -1173,7 +1114,7 @@ } }); - il.ExtLink.autolink(chatBody.find('[data-onscreenchat-body-msg]')); + tryLink(chatBody.find('[data-onscreenchat-body-msg]')); if (prepend === false) { getModule().scrollBottom(chatWindow); @@ -1602,7 +1543,7 @@ return [entry[0], proc(entry[1], entry[0])]; })); } - function piecesOf(nr, array) { + function piecesOf(nr, array){ let current = array; const result = []; while(current.length) { @@ -1611,4 +1552,24 @@ } return result; } + + function lazy(proc){ + let call = () => { + const value = proc(); + call = () => value; + return value; + }; + return () => call(); + } + + function cache(proc){ + const cached = {}; + return (...args) => { + const key = JSON.stringify(args); + if(!cached[key]){ + cached[key] = proc(...args); + } + return cached[key]; + }; + } })(jQuery, window, window.il.Chat, window.il.ChatDateTimeFormatter); diff --git a/templates/default/070-components/legacy/Modules/_component_chatroom.scss b/templates/default/070-components/legacy/Modules/_component_chatroom.scss index c48209da2b92..51c45cac7200 100755 --- a/templates/default/070-components/legacy/Modules/_component_chatroom.scss +++ b/templates/default/070-components/legacy/Modules/_component_chatroom.scss @@ -85,27 +85,50 @@ td.chatroom { height: auto; } -.chat-autocomplete { - display: flex; +.chat-autocomplete-container { position: relative; +} + +.chat-autocomplete-container .alert { + margin-top: 5px; +} + +.chat-autocomplete { + position: absolute; width: 100%; - flex-direction: column; max-height: 200px; - overflow: visible scroll; + overflow: hidden auto; + padding: 0; + margin: 0; + border: 1px solid gray; +} + +.chat-autocomplete li { + list-style: none; } .chat-autocomplete button { background-color: white; - border: 1px solid gray; text-align: left; + width: 100%; + padding: 0; + margin: 0; + border: none; } -.chat-autocomplete button:hover { - background-color: lightgray; +.chat-autocomplete button.load-more { + font-weight: bold; } +.chat-autocomplete button:hover, .chat-autocomplete button:focus { - background-color: lightgray; + background-color: #e2e8ef; + padding: 0; + margin: 0; + outline: none; + border: none; + box-shadow: none; + } #chat_users .dropdown ul.dropdown-menu { diff --git a/templates/default/delos.css b/templates/default/delos.css index d8cd1d7a4969..579dd63d3fb8 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -11909,27 +11909,49 @@ td.chatroom { height: auto; } -.chat-autocomplete { - display: flex; +.chat-autocomplete-container { position: relative; +} + +.chat-autocomplete-container .alert { + margin-top: 5px; +} + +.chat-autocomplete { + position: absolute; width: 100%; - flex-direction: column; max-height: 200px; - overflow: visible scroll; + overflow: hidden auto; + padding: 0; + margin: 0; + border: 1px solid gray; +} + +.chat-autocomplete li { + list-style: none; } .chat-autocomplete button { background-color: white; - border: 1px solid gray; text-align: left; + width: 100%; + padding: 0; + margin: 0; + border: none; } -.chat-autocomplete button:hover { - background-color: lightgray; +.chat-autocomplete button.load-more { + font-weight: bold; } +.chat-autocomplete button:hover, .chat-autocomplete button:focus { - background-color: lightgray; + background-color: #e2e8ef; + padding: 0; + margin: 0; + outline: none; + border: none; + box-shadow: none; } #chat_users .dropdown ul.dropdown-menu {