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("");
- 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 {