From 79ad52d51442519a813a09bf49a07f5ad960f7e8 Mon Sep 17 00:00:00 2001 From: Lukas Scharmer Date: Thu, 29 Aug 2024 13:55:49 +0200 Subject: [PATCH] Chatroom: Apply FR: Further revamping of Repository Chat --- components/ILIAS/Chatroom/Chatroom.php | 25 +- components/ILIAS/Chatroom/chat/Model/Room.js | 3 +- .../ILIAS/Chatroom/chat/Model/Subscriber.js | 13 + .../Chatroom/chat/SocketTasks/Disconnect.js | 2 - .../Chatroom/chat/SocketTasks/EnterRoom.js | 2 - .../ILIAS/Chatroom/chat/SocketTasks/Login.js | 3 +- .../Chatroom/chat/SocketTasks/SendMessage.js | 3 +- .../ILIAS/Chatroom/chat/SystemTasks/Ban.js | 2 +- .../ILIAS/Chatroom/classes/BuildChat.php | 157 ++ .../Chatroom/classes/class.ilChatroom.php | 21 +- .../classes/class.ilChatroomGUIHandler.php | 21 +- .../classes/class.ilChatroomTabGUIFactory.php | 2 +- .../Chatroom/classes/class.ilChatroomUser.php | 18 +- .../classes/class.ilObjChatroomGUI.php | 2 +- .../classes/gui/class.ilChatroomBanGUI.php | 4 +- .../classes/gui/class.ilChatroomClearGUI.php | 2 +- .../gui/class.ilChatroomHistoryGUI.php | 20 +- ....ilChatroomInviteUsersToPrivateRoomGUI.php | 4 +- .../classes/gui/class.ilChatroomKickGUI.php | 2 +- .../classes/gui/class.ilChatroomPollGUI.php | 2 +- .../classes/gui/class.ilChatroomViewGUI.php | 401 ++-- .../ILIAS/Chatroom/resources/chatroom.css | 190 -- .../ILIAS/Chatroom/resources/chatroom.js | 761 -------- .../Chatroom/resources/iliaschat.jquery.js | 1608 ----------------- .../resources/js/dist/Chatroom.min.js | 15 + .../Chatroom/resources/js/rollup.config.js | 44 + .../resources/js/src/ChatMessageArea.js | 257 +++ .../Chatroom/resources/js/src/ChatUsers.js | 182 ++ .../resources/js/src/ILIASConnector.js | 118 ++ .../ILIAS/Chatroom/resources/js/src/Logger.js | 37 + .../resources/js/src/ProfileImageLoader.js | 120 ++ .../resources/js/src/ServerConnector.js | 291 +++ .../ILIAS/Chatroom/resources/js/src/Type.js | 74 + .../Chatroom/resources/js/src/WatchList.js | 76 + .../resources/js/src/bindSendMessageBox.js | 64 + .../ILIAS/Chatroom/resources/js/src/bus.js | 42 + .../resources/js/src/createConfirmation.js | 55 + .../resources/js/src/expandableTextarea.js | 133 ++ .../resources/js/src/formatDateTime.js | 33 + .../ILIAS/Chatroom/resources/js/src/index.js | 27 + .../resources/js/src/inviteUserToRoom.js | 108 ++ .../ILIAS/Chatroom/resources/js/src/run.js | 191 ++ .../Chatroom/resources/js/src/sendFromURL.js | 30 + .../resources/js/src/willTranslate.js | 28 + .../templates/default/tpl.chatroom.html | 92 +- .../templates/default/tpl.chatroom_right.html | 20 +- .../tpl.chatroom_send_message_form.html | 21 +- .../templates/default/tpl.history.html | 16 +- .../OnScreenChat/resources/onscreenchat.js | 109 +- lang/ilias_de.lang | 17 +- lang/ilias_en.lang | 21 +- .../legacy/Modules/_component_chatroom.scss | 157 +- templates/default/delos.css | 147 +- 53 files changed, 2750 insertions(+), 3043 deletions(-) create mode 100644 components/ILIAS/Chatroom/classes/BuildChat.php delete mode 100755 components/ILIAS/Chatroom/resources/chatroom.css delete mode 100644 components/ILIAS/Chatroom/resources/chatroom.js delete mode 100644 components/ILIAS/Chatroom/resources/iliaschat.jquery.js create mode 100644 components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js create mode 100644 components/ILIAS/Chatroom/resources/js/rollup.config.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/ChatMessageArea.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/ChatUsers.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/ILIASConnector.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/Logger.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/ProfileImageLoader.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/ServerConnector.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/Type.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/WatchList.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/bindSendMessageBox.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/bus.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/createConfirmation.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/expandableTextarea.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/formatDateTime.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/index.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/run.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/sendFromURL.js create mode 100644 components/ILIAS/Chatroom/resources/js/src/willTranslate.js diff --git a/components/ILIAS/Chatroom/Chatroom.php b/components/ILIAS/Chatroom/Chatroom.php index 58cd5e7fe2f0..6cc12a02e1c8 100644 --- a/components/ILIAS/Chatroom/Chatroom.php +++ b/components/ILIAS/Chatroom/Chatroom.php @@ -32,15 +32,20 @@ public function init( array | \ArrayAccess &$pull, array | \ArrayAccess &$internal, ): void { - $contribute[\ILIAS\Setup\Agent::class] = static fn() => - new \ilChatroomSetupAgent( - $pull[\ILIAS\Refinery\Factory::class] - ); - $contribute[Component\Resource\PublicAsset::class] = fn() => - new Component\Resource\ComponentJS($this, "chatroom.js"); - $contribute[Component\Resource\PublicAsset::class] = fn() => - new Component\Resource\ComponentJS($this, "iliaschat.jquery.js"); - $contribute[Component\Resource\PublicAsset::class] = fn() => - new Component\Resource\ComponentCSS($this, "chatroom.css"); + $contribute[\ILIAS\Setup\Agent::class] = static fn() => new \ilChatroomSetupAgent($pull[\ILIAS\Refinery\Factory::class]); + + $files = [ + '../chat/node_modules/socket.io-client/dist/socket.io.min.js', + 'js/dist/Chatroom.min.js', + ]; + + $type = ['js' => Component\Resource\ComponentJS::class, 'css' => Component\Resource\ComponentCSS::class]; + + foreach ($files as $file) { + $class = $type[substr($file, strrpos($file, '.') + 1)]; + if (file_exists(__DIR__ . '/resources/' . $file)) { + $contribute[Component\Resource\PublicAsset::class] = fn() => new $class($this, $file); + } + } } } diff --git a/components/ILIAS/Chatroom/chat/Model/Room.js b/components/ILIAS/Chatroom/chat/Model/Room.js index 2955870363da..b85807964313 100755 --- a/components/ILIAS/Chatroom/chat/Model/Room.js +++ b/components/ILIAS/Chatroom/chat/Model/Room.js @@ -150,7 +150,8 @@ var Room = function Room(id) if(this.subscriberHasJoined(key)) { jsonSubscribers[key] = { id: _subscribers[key].getId(), - username: _subscribers[key].getName() + username: _subscribers[key].getName(), + profile_picture_visible: _subscribers[key].isProfilePictureVisible(), }; } } diff --git a/components/ILIAS/Chatroom/chat/Model/Subscriber.js b/components/ILIAS/Chatroom/chat/Model/Subscriber.js index 7e86334987ee..d9746c1e8f1e 100755 --- a/components/ILIAS/Chatroom/chat/Model/Subscriber.js +++ b/components/ILIAS/Chatroom/chat/Model/Subscriber.js @@ -20,6 +20,11 @@ function Subscriber(id) { */ var _socketIds = []; + /** + * @type {boolean} + */ + var _profile_picture_visible = false; + /** * @returns {number} */ @@ -65,6 +70,14 @@ function Subscriber(id) { } }; + this.setProfilePictureVisible = function(visible) { + _profile_picture_visible = visible; + }; + + this.isProfilePictureVisible = function() { + return _profile_picture_visible; + }; + /** * @returns {string} */ diff --git a/components/ILIAS/Chatroom/chat/SocketTasks/Disconnect.js b/components/ILIAS/Chatroom/chat/SocketTasks/Disconnect.js index 6fc5c3007b1a..9dec03a92080 100755 --- a/components/ILIAS/Chatroom/chat/SocketTasks/Disconnect.js +++ b/components/ILIAS/Chatroom/chat/SocketTasks/Disconnect.js @@ -38,8 +38,6 @@ module.exports = function() var userListAction = UserlistAction.create(splitIds[0], room.getJoinedSubscribers()); var notice = Notice.create('disconnected', splitIds[0], {username: subscriber.getName()}); - namespace.getDatabase().addHistory(notice); - Container.getLogger().info('Disconnected %s from %s of namespace %s', subscriberId, room.getId(), namespace.getName()); Container.getLogger().info('Updated user list for room %s of namespace %s', room.getId(), namespace.getName()); diff --git a/components/ILIAS/Chatroom/chat/SocketTasks/EnterRoom.js b/components/ILIAS/Chatroom/chat/SocketTasks/EnterRoom.js index d92a27ae9b13..0d1f5827bf95 100755 --- a/components/ILIAS/Chatroom/chat/SocketTasks/EnterRoom.js +++ b/components/ILIAS/Chatroom/chat/SocketTasks/EnterRoom.js @@ -34,8 +34,6 @@ module.exports = function(roomId) var messageSubscriber = 'welcome_to_chat'; var noticeSubscriber = Notice.create(messageSubscriber, roomId, subscriber); - namespace.getDatabase().addHistory(noticeRoom); - this.emit('notice', noticeSubscriber); this.broadcast.in(serverRoomId).emit('notice', noticeRoom); } diff --git a/components/ILIAS/Chatroom/chat/SocketTasks/Login.js b/components/ILIAS/Chatroom/chat/SocketTasks/Login.js index 3d7c239f4939..f9b74d93e9c3 100755 --- a/components/ILIAS/Chatroom/chat/SocketTasks/Login.js +++ b/components/ILIAS/Chatroom/chat/SocketTasks/Login.js @@ -1,7 +1,7 @@ var Container = require('../AppContainer'); var ConnectAction = require('../Model/Messages/ConnectAction'); -module.exports = function(subscriberName, subscriberId) +module.exports = function(subscriberName, subscriberId, profilePictureVisible) { var namespace = Container.getNamespace(this.nsp.name); @@ -17,6 +17,7 @@ module.exports = function(subscriberName, subscriberId) } } else { var subscriber = namespace.getSubscriber(subscriberId); + subscriber.setProfilePictureVisible(profilePictureVisible); subscriber.setName(subscriberName); subscriber.addSocketId(this.id); diff --git a/components/ILIAS/Chatroom/chat/SocketTasks/SendMessage.js b/components/ILIAS/Chatroom/chat/SocketTasks/SendMessage.js index 7be0127464f8..e7f2b7439f82 100755 --- a/components/ILIAS/Chatroom/chat/SocketTasks/SendMessage.js +++ b/components/ILIAS/Chatroom/chat/SocketTasks/SendMessage.js @@ -20,8 +20,7 @@ module.exports = function (data, roomId) { return; } - var subscriber = {id: this.subscriber.getId(), username: this.subscriber.getName()}; - + var subscriber = {id: this.subscriber.getId(), username: this.subscriber.getName(), profile_picture_visible: this.subscriber.isProfilePictureVisible()}; data.content = HTMLEscape.escape(data.content); var message = {}; diff --git a/components/ILIAS/Chatroom/chat/SystemTasks/Ban.js b/components/ILIAS/Chatroom/chat/SystemTasks/Ban.js index 9fdf8f614229..bd6db7d941e3 100755 --- a/components/ILIAS/Chatroom/chat/SystemTasks/Ban.js +++ b/components/ILIAS/Chatroom/chat/SystemTasks/Ban.js @@ -32,7 +32,7 @@ module.exports = function exports(req, res) { room.subscriberLeft(subscriberId); var userlistAction = UserlistAction.create(splitted[0], room.getJoinedSubscribers()); - var notice = Notice.create('user_kicked', splitted[0], {user: subscriber.getName()}); + var notice = Notice.create('user_banned', splitted[0], {user: subscriber.getName()}); subscriber.getSocketIds().forEach( userBannedMessageCallbackFactory(namespace, room.getId()) diff --git a/components/ILIAS/Chatroom/classes/BuildChat.php b/components/ILIAS/Chatroom/classes/BuildChat.php new file mode 100644 index 000000000000..719b85b9b7d9 --- /dev/null +++ b/components/ILIAS/Chatroom/classes/BuildChat.php @@ -0,0 +1,157 @@ + $room_tpl->setVariable($var, json_encode($value)); + $set_json_var('BASEURL', $this->settings->generateClientUrl()); + $set_json_var('INSTANCE', $this->settings->getInstance()); + $set_json_var('SCOPE', $this->room->getRoomId()); + $set_json_var('POSTURL', ILIAS_HTTP_PATH . '/' . $this->ilCtrl->getLinkTarget($this->gui, 'postMessage', '', true)); + $room_tpl->setVariable('JS_CALL', 'il.Chatroom.' . ($read_only ? 'runReadOnly' : 'run')); + + $set_json_var('INITIAL_DATA', $initial); + $set_json_var('INITIAL_USERS', $this->room->getConnectedUsers()); + $set_json_var('DATE_FORMAT', (string) $this->user->getDateFormat()); + $set_json_var('TIME_FORMAT', $this->timeFormat()); + + $room_tpl->setVariable('CHAT_OUTPUT', $output); + $room_tpl->setVariable('CHAT_INPUT', $input); + + $this->renderLanguageVariables($room_tpl); + + return $room_tpl; + } + + public function initialData(array $users, bool $show_auto_messages, ?string $redirect_url, array $userinfo, array $messages): array + { + $initial = []; + $initial['users'] = $users; + $initial['redirect_url'] = $redirect_url; + $initial['profile_image_url'] = $this->ilCtrl->getLinkTarget($this->gui, 'view-getUserProfileImages', '', true); + $initial['no_profile_image_url'] = ilUtil::getImagePath('placeholder/no_photo_xxsmall.jpg'); + $initial['subdirectory'] = $this->settings->getSubDirectory(); + + $initial['userinfo'] = $userinfo; + $initial['messages'] = $messages; + + $initial['state'] = [ + 'scrolling' => true, + 'show_auto_msg' => $show_auto_messages, + 'system_message_update_url' => $this->ilCtrl->getFormAction($this->gui, 'view-toggleAutoMessageDisplayState', '', true, false), + ]; + + return $initial; + } + + private function renderLanguageVariables(ilTemplate $room_tpl): void + { + $set_vars = fn($a) => array_map($room_tpl->setVariable(...), array_keys($a), array_values($a)); + + $js_translations = [ + 'LBL_MAINROOM' => 'chat_mainroom', + 'LBL_JOIN' => 'chat_join', + 'LBL_INVITE_TO_PRIVATE_ROOM' => 'invite_to_private_room', + 'LBL_KICK' => 'chat_kick', + 'LBL_BAN' => 'chat_ban', + 'LBL_KICK_QUESTION' => 'kick_question', + 'LBL_BAN_QUESTION' => 'ban_question', + 'LBL_ADDRESS' => 'chat_address', + 'LBL_WHISPER' => 'chat_whisper', + 'LBL_CONNECT' => 'chat_connection_established', + 'LBL_DISCONNECT' => 'chat_connection_disconnected', + 'LBL_TO_MAINROOM' => 'chat_to_mainroom', + 'LBL_WELCOME_TO_CHAT' => 'welcome_to_chat', + 'LBL_USER_INVITED' => 'user_invited', + 'LBL_USER_KICKED' => 'user_kicked', + 'LBL_USER_BANNED' => 'user_banned', + 'LBL_USER_INVITED_SELF' => 'user_invited_self', + 'LBL_PRIVATE_ROOM_CLOSED' => 'private_room_closed', + 'LBL_PRIVATE_ROOM_ENTERED' => 'private_room_entered', + 'LBL_PRIVATE_ROOM_LEFT' => 'private_room_left', + 'LBL_PRIVATE_ROOM_ENTERED_USER' => 'private_room_entered_user', + 'LBL_KICKED_FROM_PRIVATE_ROOM' => 'kicked_from_private_room', + 'LBL_OK' => 'ok', + 'LBL_DELETE' => 'delete', + 'LBL_INVITE' => 'chat_invite', + 'LBL_CANCEL' => 'cancel', + 'LBL_HISTORY_CLEARED' => 'history_cleared', + 'LBL_CLEAR_ROOM_HISTORY' => 'clear_room_history', + 'LBL_CLEAR_ROOM_HISTORY_QUESTION' => 'clear_room_history_question', + 'LBL_END_WHISPER' => 'end_whisper', + 'LBL_TIMEFORMAT' => 'lang_timeformat_no_sec', + 'LBL_DATEFORMAT' => 'lang_dateformat', + 'LBL_START_PRIVATE_CHAT' => 'start_private_chat', + ]; + + $set_vars(array_map( + fn($v) => json_encode($this->ilLng->txt($v), JSON_THROW_ON_ERROR), + $js_translations + )); + + $this->ilLng->toJSMap([ + 'chat_user_x_is_typing' => $this->ilLng->txt('chat_user_x_is_typing'), + 'chat_users_are_typing' => $this->ilLng->txt('chat_users_are_typing'), + ]); + + $vars = [ + 'LBL_LAYOUT' => 'layout', + 'LBL_SHOW_SETTINGS' => 'show_settings', + 'LBL_USER_IN_ROOM' => 'user_in_room', + 'LOADING_IMAGE' => 'media/loader.svg', + ]; + + $set_vars(array_map($this->ilLng->txt(...), $vars)); + $room_tpl->setVariable('LOADING_IMAGE', ilUtil::getImagePath('media/loader.svg')); + } + + private function timeFormat(): string + { + return match ($this->user->getTimeFormat()) { + (string) ilCalendarSettings::TIME_FORMAT_12 => 'h:ia', + default => 'H:i', + }; + } +} diff --git a/components/ILIAS/Chatroom/classes/class.ilChatroom.php b/components/ILIAS/Chatroom/classes/class.ilChatroom.php index 57ce9bffbec4..26d9f26d03ae 100755 --- a/components/ILIAS/Chatroom/classes/class.ilChatroom.php +++ b/components/ILIAS/Chatroom/classes/class.ilChatroom.php @@ -292,7 +292,8 @@ public function connectUser(ilChatroomUser $user): bool $userdata = [ 'login' => $user->getUsername(), - 'id' => $user->getUserId() + 'id' => $user->getUserId(), + 'profile_picture_visible' => $user->isProfilePictureVisible(), ]; $query = 'SELECT user_id FROM ' . self::$userTable . ' WHERE room_id = %s AND user_id = %s'; @@ -331,7 +332,7 @@ public function getConnectedUsers(bool $only_data = true): array $users = []; while ($row = $DIC->database()->fetchAssoc($rset)) { - $users[] = $only_data ? json_decode($row['userdata'], false, 512, JSON_THROW_ON_ERROR) : $row; + $users[] = $only_data ? json_decode($row['userdata'], true, 512, JSON_THROW_ON_ERROR) : $row; } return $users; @@ -757,16 +758,18 @@ public function getLastMessages(int $number, ilChatroomUser $chatuser): array // by sql. So we fetch twice as much as we need and hope that there // are not more than $number private messages. $DIC->database()->setLimit($number); - $rset = $DIC->database()->query( + $rset = $DIC->database()->queryF( 'SELECT * FROM ' . self::$historyTable . ' - WHERE room_id = ' . $DIC->database()->quote($this->roomId, ilDBConstants::T_INTEGER) . ' + WHERE room_id = %s AND ( - (' . $DIC->database()->like('message', ilDBConstants::T_TEXT, '%"type":"message"%') . ' AND NOT ' . $DIC->database()->like('message', ilDBConstants::T_TEXT, '%"public":0%') . ') - OR ' . $DIC->database()->like('message', ilDBConstants::T_TEXT, '%"target":{%"id":"' . $chatuser->getUserId() . '"%') . ' - OR ' . $DIC->database()->like('message', ilDBConstants::T_TEXT, '%"from":{"id":' . $chatuser->getUserId() . '%') . ' + (JSON_VALUE(message, "$.type") = "message" AND (NOT JSON_CONTAINS_PATH(message, "one", "$.target.public") OR JSON_VALUE(message, "$.target.public") <> 0)) + OR JSON_VALUE(message, "$.target.id") = %s + OR JSON_VALUE(message, "$.from.id") = %s ) - ORDER BY timestamp DESC' + ORDER BY timestamp DESC', + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [$this->roomId, $chatuser->getUserId(), $chatuser->getUserId()] ); $result_count = 0; @@ -789,7 +792,7 @@ public function getLastMessages(int $number, ilChatroomUser $chatuser): array 'SELECT * FROM ' . self::$historyTable . ' WHERE room_id = %s - AND ' . $DIC->database()->like('message', ilDBConstants::T_TEXT, '%%"type":"notice"%%') . ' + AND JSON_VALUE(message, "$.type") = "notice" AND timestamp <= %s AND timestamp >= %s ORDER BY timestamp DESC', [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], diff --git a/components/ILIAS/Chatroom/classes/class.ilChatroomGUIHandler.php b/components/ILIAS/Chatroom/classes/class.ilChatroomGUIHandler.php index 119eb1162de2..db9d5e571d98 100755 --- a/components/ILIAS/Chatroom/classes/class.ilChatroomGUIHandler.php +++ b/components/ILIAS/Chatroom/classes/class.ilChatroomGUIHandler.php @@ -108,7 +108,7 @@ public function execute(string $method): void { $this->ilLng->loadLanguageModule('chatroom'); - if (method_exists($this, $method)) { + if (is_callable([$this, $method])) { $this->$method(); return; } @@ -157,7 +157,7 @@ protected function getRoomByObjectId(int $objectId): ?ilChatroom protected function exitIfNoRoomExists(?ilChatroom $room): void { if (null === $room) { - $this->sendResponse([ + $this->sendJSONResponse([ 'success' => false, 'reason' => 'unknown room', ]); @@ -165,15 +165,22 @@ protected function exitIfNoRoomExists(?ilChatroom $room): void } /** - * Sends a json encoded response and exits the php process - * @param mixed $response + * Sends a json encoded response and exits the php process. */ - public function sendResponse($response, bool $isJson = false): void + protected function sendJSONResponse($response): void + { + $this->sendResponse(json_encode($response), 'application/json'); + } + + /** + * Sends a response and exits the php process. + */ + protected function sendResponse(string $content, string $type): void { $this->http->saveResponse( $this->http->response() - ->withHeader(ResponseHeader::CONTENT_TYPE, 'application/json') - ->withBody(Streams::ofString($isJson ? $response : json_encode($response, JSON_THROW_ON_ERROR))) + ->withHeader(ResponseHeader::CONTENT_TYPE, $type) + ->withBody(Streams::ofString($content)) ); $this->http->sendResponse(); $this->http->close(); diff --git a/components/ILIAS/Chatroom/classes/class.ilChatroomTabGUIFactory.php b/components/ILIAS/Chatroom/classes/class.ilChatroomTabGUIFactory.php index b00ce58d1501..b7c5b112a92e 100755 --- a/components/ILIAS/Chatroom/classes/class.ilChatroomTabGUIFactory.php +++ b/components/ILIAS/Chatroom/classes/class.ilChatroomTabGUIFactory.php @@ -293,7 +293,7 @@ public function getTabsForCommand(string $command): void $config = [ 'view' => [ - 'lng' => 'view', + 'lng' => 'obj_chtr', 'link' => $DIC->ctrl()->getLinkTarget($this->gui, 'view'), 'permission' => 'read' ], diff --git a/components/ILIAS/Chatroom/classes/class.ilChatroomUser.php b/components/ILIAS/Chatroom/classes/class.ilChatroomUser.php index 57cb172ab291..8a5417b18909 100755 --- a/components/ILIAS/Chatroom/classes/class.ilChatroomUser.php +++ b/components/ILIAS/Chatroom/classes/class.ilChatroomUser.php @@ -27,6 +27,7 @@ class ilChatroomUser { private string $username = ''; + private ?bool $profile_picture_visible = null; public function __construct(private readonly ilObjUser $user, private readonly ilChatroom $room) { @@ -79,7 +80,7 @@ public function getUsername(): string return $session[$this->room->getRoomId()]['username']; } - return $this->user->getLogin(); + return $this->user->getPublicName(); } /** @@ -94,6 +95,21 @@ public function setUsername(string $username): void ilSession::set('chat', $session); } + public function isProfilePictureVisible(): bool + { + if ($this->profile_picture_visible === null) { + $this->profile_picture_visible = ilSession::get('chat')[$this->room->getRoomId()]['profile-picture-visible'] ?? false; + } + return $this->profile_picture_visible; + } + + public function setProfilePictureVisible(bool $show_it): void + { + $session = ilSession::get('chat'); + $session[$this->room->getRoomId()]['profile-picture-visible'] = $show_it; + ilSession::set('chat', $session); + } + /** * Returns an array of chat-name suggestions * @return array diff --git a/components/ILIAS/Chatroom/classes/class.ilObjChatroomGUI.php b/components/ILIAS/Chatroom/classes/class.ilObjChatroomGUI.php index 5ac274717547..5251f06f751e 100755 --- a/components/ILIAS/Chatroom/classes/class.ilObjChatroomGUI.php +++ b/components/ILIAS/Chatroom/classes/class.ilObjChatroomGUI.php @@ -305,7 +305,7 @@ protected function afterSave(ilObject $new_object): void $room->saveSettings([ 'object_id' => $new_object->getId(), 'autogen_usernames' => 'Autogen #', - 'display_past_msgs' => 20, + 'display_past_msgs' => 100, ]); $connector = $this->getConnector(); diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomBanGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomBanGUI.php index f869228fb407..40d1614577bd 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomBanGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomBanGUI.php @@ -57,7 +57,7 @@ public function __construct( parent::__construct($gui); } - private function handleTableActions(): void + public function handleTableActions(): void { $action = $this->http->wrapper()->query()->retrieve( 'chat_ban_table_action', @@ -159,6 +159,6 @@ public function active(): void $room->disconnectUser($userToBan); } - $this->sendResponse($response); + $this->sendResponse($response, 'application/json'); } } diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomClearGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomClearGUI.php index d93f578a979f..0f42bc1ff6f7 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomClearGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomClearGUI.php @@ -41,6 +41,6 @@ public function executeDefault(string $requestedMethod): void $connector = $this->gui->getConnector(); $response = $connector->sendClearMessages($room->getRoomId(), $chat_user->getUserId()); - $this->sendResponse($response); + $this->sendResponse($response, 'application/json'); } } diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomHistoryGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomHistoryGUI.php index c6b575d9506a..69bdb0618dc4 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomHistoryGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomHistoryGUI.php @@ -108,7 +108,7 @@ public function byDay(bool $export = false): void $durationForm->setValuesByPost(); } - $this->showMessages($messages, $durationForm, $export, $from, $to); + $this->showMessages($messages, $durationForm, $export, $from, $to, $room); } private function showMessages( @@ -116,13 +116,14 @@ private function showMessages( ilPropertyFormGUI $durationForm, bool $export = false, ?ilDateTime $from = null, - ?ilDateTime $to = null + ?ilDateTime $to = null, + $room = null ): void { $this->redirectIfNoPermission('read'); $this->gui->switchToVisibleMode(); - $this->mainTpl->addCss('components/ILIAS/Chatroom/templates/default/style.css'); + $this->mainTpl->addCss('assets/css/chatroom.css'); // should be able to grep templates if ($export) { @@ -140,8 +141,9 @@ private function showMessages( $num_messages_shown = 0; $prev_date_time_presentation = null; $prev_date_time = null; - foreach ($messages as $message) { - switch ($message['message']->type) { + if ($export) { + foreach ($messages as $message) { + switch ($message['message']->type) { case 'message': $message_date = new ilDate($message['timestamp'], IL_CAL_UNIX); $message_date_time = new ilDateTime($message['timestamp'], IL_CAL_UNIX); @@ -167,6 +169,7 @@ private function showMessages( ++$num_messages_shown; break; + } } } @@ -209,6 +212,13 @@ private function showMessages( $roomTpl->setVariable('PERIOD_FORM', $durationForm->getHTML()); + if ($room && $messages !== []) { + $this->mainTpl->addJavaScript('assets/js/socket.io.js'); + $this->mainTpl->addJavaScript('assets/js/Chatroom.min.js'); + $roomTpl->setVariable('CHAT', (new ilChatroomViewGUI($this->gui))->readOnlyChatWindow($room, array_column($messages, 'message'))->get()); + } else { + $roomTpl->setVariable('CHAT', ''); + } $this->mainTpl->setVariable('ADM_CONTENT', $roomTpl->get()); } diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomInviteUsersToPrivateRoomGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomInviteUsersToPrivateRoomGUI.php index 232744a92aed..89918a39ddd9 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomInviteUsersToPrivateRoomGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomInviteUsersToPrivateRoomGUI.php @@ -56,7 +56,7 @@ private function inviteById(int $invited_id): void $room->sendInvitationNotification($this->gui, $chat_user, $invited_id); if ('asynch' === $this->getRequestValue('cmdMode', $this->refinery->kindlyTo()->string())) { - $this->sendResponse($response); + $this->sendResponse($response, 'application/json'); } $this->ilCtrl->redirect($this->gui, 'view'); } @@ -87,6 +87,6 @@ public function getUserList(): void $auto->setResultField('login'); $auto->enableFieldSearchableCheck(true); - $this->sendResponse($auto->getList($query), true); + $this->sendResponse($auto->getList($query), 'application/json'); } } diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomKickGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomKickGUI.php index 06f09efdbaf9..e063405195fd 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomKickGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomKickGUI.php @@ -60,6 +60,6 @@ public function main(): void $room->disconnectUser($userToKick); } - $this->sendResponse($response); + $this->sendResponse($response, 'application/json'); } } diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomPollGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomPollGUI.php index 91859376fb9d..5365589dda3d 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomPollGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomPollGUI.php @@ -28,6 +28,6 @@ class ilChatroomPollGUI extends ilChatroomGUIHandler { public function executeDefault(string $requestedMethod): void { - $this->sendResponse(['success' => true]); + $this->sendJSONResponse(['success' => true]); } } diff --git a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php index 6bfed23fdd1c..f7b60f4b9d73 100755 --- a/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php +++ b/components/ILIAS/Chatroom/classes/gui/class.ilChatroomViewGUI.php @@ -21,12 +21,14 @@ use ILIAS\Filesystem\Stream\Streams; use ILIAS\HTTP\Response\ResponseHeader; use ILIAS\UI\Component\Component; +use ILIAS\Chatroom\BuildChat; +use ILIAS\UI\Component\Button\Button; /** * Class ilChatroomViewGUI * @author Jan Posselt * @version $Id$ - * @ingroup components\ILIASChatroom + * @ingroup ModulesChatroom */ class ilChatroomViewGUI extends ilChatroomGUIHandler { @@ -40,12 +42,14 @@ public function joinWithCustomName(): void $chat_user = new ilChatroomUser($this->ilUser, $room); $failure = true; $username = ''; + $custom_username = false; if ($this->hasRequestValue('custom_username_radio')) { if ( $this->hasRequestValue('custom_username_text') && $this->getRequestValue('custom_username_radio', $this->refinery->kindlyTo()->string()) === 'custom_username' ) { + $custom_username = true; $username = $this->getRequestValue('custom_username_text', $this->refinery->kindlyTo()->string()); $failure = false; } elseif ( @@ -64,6 +68,7 @@ public function joinWithCustomName(): void if (!$failure && trim($username) !== '') { if (!$room->isSubscribed($chat_user->getUserId())) { $chat_user->setUsername($chat_user->buildUniqueUsername($username)); + $chat_user->setProfilePictureVisible(!$custom_username); } $this->showRoom($room, $chat_user); @@ -78,8 +83,8 @@ public function joinWithCustomName(): void */ private function setupTemplate(): void { - $this->mainTpl->addJavaScript('assets/js/chatroom.js'); - $this->mainTpl->addJavaScript('assets/js/iliaschat.jquery.js'); + $this->mainTpl->addJavaScript('assets/js/socket.io.min.js'); + $this->mainTpl->addJavaScript('assets/js/Chatroom.min.js'); $this->mainTpl->addJavaScript('assets/js/AdvancedSelectionList.js'); $this->mainTpl->addCss('assets/css/chatroom.css'); @@ -127,60 +132,44 @@ private function showRoom(ilChatroom $room, ilChatroomUser $chat_user): void $this->ilCtrl->redirectByClass(ilInfoScreenGUI::class, 'showSummary'); } - $settings = $connector->getSettings(); - - $initial = new stdClass(); - $initial->users = $room->getConnectedUsers(); - $initial->redirect_url = $this->ilCtrl->getLinkTarget($this->gui, 'view-lostConnection', '', false); - $initial->profile_image_url = $this->ilCtrl->getLinkTarget($this->gui, 'view-getUserProfileImages', '', true); - $initial->no_profile_image_url = ilUtil::getImagePath('placeholder/no_photo_xxsmall.jpg'); - $initial->subdirectory = $settings->getSubDirectory(); - - $initial->userinfo = [ - 'moderator' => ilChatroom::checkUserPermissions('moderate', $ref_id, false), - 'id' => $chat_user->getUserId(), - 'login' => $chat_user->getUsername(), - 'broadcast_typing' => $chat_user->enabledBroadcastTyping(), - ]; - - $initial->messages = []; - - if ((int) $room->getSetting('display_past_msgs')) { - $initial->messages = array_merge( - $initial->messages, - array_reverse($room->getLastMessages($room->getSetting('display_past_msgs'), $chat_user)) - ); - } - - $roomTpl = new ilTemplate('tpl.chatroom.html', true, true, 'components/ILIAS/Chatroom'); - $roomTpl->setVariable('BASEURL', $settings->generateClientUrl()); - $roomTpl->setVariable('INSTANCE', $settings->getInstance()); - $roomTpl->setVariable('SCOPE', $scope); - $roomTpl->setVariable('POSTURL', $this->ilCtrl->getLinkTarget($this->gui, 'postMessage', '', true)); - - $roomTpl->setVariable('ACTIONS', $this->ilLng->txt('actions')); - $roomTpl->setVariable('LBL_USER', $this->ilLng->txt('user')); - $roomTpl->setVariable('LBL_USER_TEXT', $this->ilLng->txt('invite_username')); - $showAutoMessages = true; - if ($this->ilUser->getPref('chat_hide_automsg_' . $room->getRoomId())) { - $showAutoMessages = false; - } - - $initial->state = new stdClass(); - $initial->state->scrolling = true; - $initial->state->show_auto_msg = $showAutoMessages; - - $roomTpl->setVariable('INITIAL_DATA', json_encode($initial, JSON_THROW_ON_ERROR)); - $roomTpl->setVariable('INITIAL_USERS', json_encode($room->getConnectedUsers(), JSON_THROW_ON_ERROR)); - $roomTpl->setVariable('CHAT_OUTPUT', $this->panel($this->ilLng->txt('messages'), $this->legacy('
'))); - $roomTpl->setVariable('CHAT_INPUT', $this->panel($this->ilLng->txt('write_message'), $this->sendMessageForm())); - - $this->renderLanguageVariables($roomTpl); - - ilModalGUI::initJS(); + $messages = $room->getSetting('display_past_msgs') ? array_reverse(array_filter( + $room->getLastMessages($room->getSetting('display_past_msgs'), $chat_user), + fn($entry) => $entry->type !== 'notice' + )) : []; + + $is_moderator = ilChatroom::checkUserPermissions('moderate', $ref_id, false); + $show_auto_messages = !$this->ilUser->getPref('chat_hide_automsg_' . $room->getRoomId()); + + $build = $this->buildChat($room, $connector->getSettings()); + + $room_tpl = $build->template(false, $build->initialData( + $room->getConnectedUsers(), + $show_auto_messages, + $this->ilCtrl->getLinkTarget($this->gui, 'view-lostConnection', '', false), + [ + 'moderator' => $is_moderator, + 'id' => $chat_user->getUserId(), + 'login' => $chat_user->getUsername(), + 'broadcast_typing' => $chat_user->enabledBroadcastTyping(), + 'profile_picture_visible' => $chat_user->isProfilePictureVisible(), + ], + $messages + ), $this->panel($this->ilLng->txt('write_message'), $this->sendMessageForm()), $this->panel($this->ilLng->txt('messages'), $this->legacy('
'))); + + $this->mainTpl->setContent($room_tpl->get()); + $this->mainTpl->setRightContent($this->userList() . $this->chatFunctions($show_auto_messages, $is_moderator)); + } - $this->mainTpl->setContent($roomTpl->get()); - $this->mainTpl->setRightContent($this->userList() . $this->chatFunctions($showAutoMessages)); + public function readOnlyChatWindow(ilChatroom $room, array $messages): ilTemplate + { + $build = $this->buildChat($room, $this->gui->getConnector()->getSettings()); + + return $build->template(true, $build->initialData([], true, null, [ + 'moderator' => false, + 'id' => -1, + 'login' => null, + 'broadcast_typing' => false, + ], $messages), $this->panel($this->ilLng->txt('messages'), $this->legacy('
')), ''); } private function sendMessageForm(): Component @@ -199,53 +188,59 @@ private function userList(): string return $this->panel($this->ilLng->txt('users'), $this->legacy($roomRightTpl->get())); } - private function chatFunctions(bool $showAutoMessages): string + private function chatFunctions(bool $show_auto_messages, bool $is_moderator): string { - $auto_scroll = $this - ->uiFactory - ->button() - ->toggle( - $this->ilLng->txt('auto_scroll'), - '#', - '#', - true - ) - ->withAriaLabel($this->ilLng->txt('auto_scroll')) - ->withOnLoadCode(static function (string $id): string { - return '$("#' . $id . '").on("click", function(e) { - let t = $(this), msg = $("#chat_messages"); - if (t.hasClass("on")) { - msg.trigger("msg-scrolling:toggle", [true]); - } else { - msg.trigger("msg-scrolling:toggle", [false]); - } - });'; - }); - - $toggleUrl = $this->ilCtrl->getFormAction($this->gui, 'view-toggleAutoMessageDisplayState', '', true, false); - $messages = $this - ->uiFactory - ->button() - ->toggle( - $this->ilLng->txt('chat_show_auto_messages'), - '#', - '#', - $showAutoMessages - ) - ->withAriaLabel($this->ilLng->txt('chat_show_auto_messages')) - ->withOnLoadCode(static function (string $id) use ($toggleUrl): string { - return '$("#' . $id . '").on("click", function(e) { - let t = $(this), msg = $("#chat_messages"); - if (t.hasClass("on")) { - msg.trigger("auto-message:toggle", [true, "' . $toggleUrl . '"]); - } else { - msg.trigger("auto-message:toggle", [false, "' . $toggleUrl . '"]); - } - });'; - }); - - return $this->panel($this->ilLng->txt('chat_functions'), [ - $this->legacy('
'), + $txt = $this->ilLng->txt(...); + $js_escape = json_encode(...); + $format = fn($format, ...$args) => sprintf($format, ...array_map($js_escape, $args)); + $register = fn($name, $c) => $c->withOnLoadCode(fn($id) => $format( + 'il.Chatroom.bus.send(%s, document.getElementById(%s));', + $name, + $id + )); + + $b = $this->uiFactory->button(); + $toggle = fn($label, $enabled) => $b->toggle($label, '#', '#', $enabled)->withAriaLabel($label); + + $bind = fn($key, $m) => $m->withAdditionalOnLoadCode(fn(string $id) => $format( + '$(() => il.Chatroom.bus.send(%s, { + node: document.getElementById(%s), + showModal: () => $(document).trigger(%s, {}), + closeModal: () => $(document).trigger(%s, {}) + }));', + $key, + $id, + $m->getShowSignal()->getId(), + $m->getCloseSignal()->getId() + )); + + $interrupt = fn($key, $label, $text, $button = null) => $bind($key, $this->uiFactory->modal()->interruptive( + $label, + $text, + '' + ))->withActionButtonLabel($button ?? $label); + + $auto_scroll = $register('auto-scroll-toggle', $toggle($txt('auto_scroll'), true)); + $messages = $register('system-messages-toggle', $toggle($txt('chat_show_auto_messages'), $show_auto_messages)); + + $invite = $bind('invite-modal', $this->uiFactory->modal()->roundtrip($txt('chat_invite'), $this->legacy($txt('invite_to_private_room')), [ + $this->uiFactory->input()->field()->text($txt('chat_invite')), + ])->withSubmitLabel($txt('chat_invite'))); + + $buttons = []; + $buttons[] = $register('invite-button', $b->shy($txt('invite_to_private_room'), '')); + if ($is_moderator) { + $buttons[] = $register('clear-history-button', $b->shy($txt('clear_room_history'), '')); + } + + return $this->panel($txt('chat_functions'), [ + $this->legacy('
'), + ...$buttons, + $invite, + $interrupt('kick-modal', $txt('chat_kick'), $txt('kick_question')), + $interrupt('ban-modal', $txt('chat_ban'), $txt('ban_question')), + $interrupt('clear-history-modal', $txt('clear_room_history'), $txt('clear_room_history_question')), + $this->legacy('
'), $this->legacy(sprintf('
%s%s
', $this->checkbox($auto_scroll), $this->checkbox($messages))), ]); } @@ -260,9 +255,15 @@ private function legacy(string $html): Component return $this->uiFactory->legacy($html); } + /** + * @param Component|array $body + */ private function panel(string $title, $body): string { - $panel = $this->uiFactory->panel()->standard($title, $body); + if (is_array($body)) { + $body = $this->uiFactory->legacy(join('', array_map($this->uiRenderer->render(...), $body))); + } + $panel = $this->uiFactory->panel()->secondary()->legacy($title, $body); return $this->uiRenderer->render($panel); } @@ -303,64 +304,10 @@ private function cancelJoin(string $message): void protected function renderSendMessageBox(ilTemplate $roomTpl): void { - $roomTpl->setVariable('LBL_TOALL', $this->ilLng->txt('chat_message_to_all')); + $roomTpl->setVariable('PLACEHOLDER', $this->ilLng->txt('chat_osc_write_a_msg')); $roomTpl->setVariable('LBL_SEND', $this->ilLng->txt('send')); } - protected function renderLanguageVariables(ilTemplate $roomTpl): void - { - $js_translations = [ - 'LBL_MAINROOM' => 'chat_mainroom', - 'LBL_LEFT_PRIVATE_ROOM' => 'left_private_room', - 'LBL_JOIN' => 'chat_join', - 'LBL_INVITE_TO_PRIVATE_ROOM' => 'invite_to_private_room', - 'LBL_KICK' => 'chat_kick', - 'LBL_BAN' => 'chat_ban', - 'LBL_KICK_QUESTION' => 'kick_question', - 'LBL_BAN_QUESTION' => 'ban_question', - 'LBL_ADDRESS' => 'chat_address', - 'LBL_WHISPER' => 'chat_whisper', - 'LBL_CONNECT' => 'chat_connection_established', - 'LBL_DISCONNECT' => 'chat_connection_disconnected', - 'LBL_TO_MAINROOM' => 'chat_to_mainroom', - 'LBL_WELCOME_TO_CHAT' => 'welcome_to_chat', - 'LBL_USER_INVITED' => 'user_invited', - 'LBL_USER_KICKED' => 'user_kicked', - 'LBL_USER_INVITED_SELF' => 'user_invited_self', - 'LBL_PRIVATE_ROOM_CLOSED' => 'private_room_closed', - 'LBL_PRIVATE_ROOM_ENTERED' => 'private_room_entered', - 'LBL_PRIVATE_ROOM_LEFT' => 'private_room_left', - 'LBL_PRIVATE_ROOM_ENTERED_USER' => 'private_room_entered_user', - 'LBL_KICKED_FROM_PRIVATE_ROOM' => 'kicked_from_private_room', - 'LBL_OK' => 'ok', - 'LBL_DELETE' => 'delete', - 'LBL_INVITE' => 'chat_invite', - 'LBL_CANCEL' => 'cancel', - 'LBL_WHISPER_TO' => 'whisper_to', - 'LBL_SPEAK_TO' => 'speak_to', - 'LBL_HISTORY_CLEARED' => 'history_cleared', - 'LBL_CLEAR_ROOM_HISTORY' => 'clear_room_history', - 'LBL_CLEAR_ROOM_HISTORY_QUESTION' => 'clear_room_history_question', - 'LBL_END_WHISPER' => 'end_whisper', - 'LBL_TIMEFORMAT' => 'lang_timeformat_no_sec', - 'LBL_DATEFORMAT' => 'lang_dateformat', - ]; - foreach ($js_translations as $placeholder => $lng_variable) { - $roomTpl->setVariable($placeholder, json_encode($this->ilLng->txt($lng_variable), JSON_THROW_ON_ERROR)); - } - $this->ilLng->toJSMap([ - 'chat_user_x_is_typing' => $this->ilLng->txt('chat_user_x_is_typing'), - 'chat_users_are_typing' => $this->ilLng->txt('chat_users_are_typing'), - ]); - - $roomTpl->setVariable('LBL_LAYOUT', $this->ilLng->txt('layout')); - $roomTpl->setVariable('LBL_SHOW_SETTINGS', $this->ilLng->txt('show_settings')); - $roomTpl->setVariable('LBL_USER_IN_ROOM', $this->ilLng->txt('user_in_room')); - $roomTpl->setVariable('LBL_USER_IN_ILIAS', $this->ilLng->txt('user_in_ilias')); - $roomTpl->setVariable('LBL_NO_USER', $this->ilLng->txt('msg_no_search_result')); - $roomTpl->setVariable('LOADING_IMAGE', ilUtil::getImagePath('media/loader.svg')); - } - protected function renderRightUsersBlock(ilTemplate $roomTpl): void { $roomTpl->setVariable('LBL_NO_FURTHER_USERS', $this->ilLng->txt('no_further_users')); @@ -417,7 +364,8 @@ public function executeDefault(string $requestedMethod): void $this->showNameSelection($chat_user); } } else { - $chat_user->setUsername($this->ilUser->getLogin()); + $chat_user->setUsername($this->ilUser->getPublicName()); + $chat_user->setProfilePictureVisible(true); $this->showRoom($room, $chat_user); } } @@ -450,65 +398,110 @@ public function getUserProfileImages(): void $response = []; - $usr_ids = null; - if ($this->hasRequestValue('usr_ids')) { - $usr_ids = $this->getRequestValue('usr_ids', $this->refinery->kindlyTo()->string()); - } - if (null === $usr_ids || '' === $usr_ids) { - $this->sendResponse($response); - } - - $this->ilLng->loadLanguageModule('user'); + $request = json_decode($this->http->request()->getBody()->getContents(), true); ilWACSignedPath::setTokenMaxLifetimeInSeconds(30); - $user_ids = array_filter(array_map('intval', array_map('trim', explode(',', (string) $usr_ids)))); + $users = $this->refinery->kindlyTo()->listOf($this->refinery->byTrying([ + $this->refinery->kindlyTo()->recordOf([ + 'id' => $this->refinery->kindlyTo()->int(), + 'username' => $this->refinery->kindlyTo()->string(), + 'profile_picture_visible' => $this->refinery->kindlyTo()->bool(), + ]), + $this->refinery->kindlyTo()->recordOf([ + 'id' => $this->refinery->kindlyTo()->int(), + 'username' => $this->refinery->kindlyTo()->string(), + ]), + ]))->transform($request['profiles'] ?? []); - $room = ilChatroom::byObjectId($this->gui->getObject()->getId()); - $chatRoomUserDetails = ilChatroomUser::getUserInformation($user_ids, $room->getRoomId()); - $chatRoomUserDetailsByUsrId = array_combine( - array_map( - static function (stdClass $userData): int { - return (int) $userData->id; - }, - $chatRoomUserDetails - ), - $chatRoomUserDetails - ); + $user_ids = array_column($users, 'id'); $public_data = ilUserUtil::getNamePresentation($user_ids, true, false, '', false, true, false, true); - $public_names = ilUserUtil::getNamePresentation($user_ids, false, false, '', false, true, false, false); - - foreach ($user_ids as $usr_id) { - if (!array_key_exists($usr_id, $chatRoomUserDetailsByUsrId)) { - continue; - } - if ($room->getSetting('allow_custom_usernames')) { + foreach ($users as $user) { + if ($user['profile_picture_visible'] ?? false) { + $public_image = $public_data[$user['id']]['img'] ?? ''; + } else { /** @var ilUserAvatar $avatar */ $avatar = $DIC["user.avatar.factory"]->avatar('xsmall'); $avatar->setUsrId(ANONYMOUS_USER_ID); - $avatar->setName(ilStr::subStr($chatRoomUserDetailsByUsrId[$usr_id]->login, 0, 2)); - - $public_name = $chatRoomUserDetailsByUsrId[$usr_id]->login; + $avatar->setName(ilStr::subStr($user['username'], 0, 2)); $public_image = $avatar->getUrl(); - } else { - $public_image = $public_data[$usr_id]['img'] ?? ''; - $public_name = ''; - if (isset($public_names[$usr_id])) { - $public_name = $public_names[$usr_id]; - if (isset($public_data[$usr_id]['login']) && 'unknown' === $public_name) { - $public_name = $public_data[$usr_id]['login']; - } - } } - $response[$usr_id] = [ - 'public_name' => $public_name, - 'profile_image' => $public_image, - ]; + $response[json_encode($user)] = $public_image; } - $this->sendResponse($response); + $this->sendJSONResponse($response); + } + + public function userEntry(): void + { + global $DIC; + + $kindly = $this->refinery->kindlyTo(); + $s = $kindly->string(); + $int = $kindly->int(); + $get = $this->http->wrapper()->query()->retrieve(...); + $get_or = fn($k, $t, $d = null) => $get($k, $this->refinery->byTrying([$t, $this->refinery->always($d)])); + + $ref_id = $get('ref_id', $int); + $user_id = $get('user_id', $int); + $username = $get('username', $s); + $actions = $get_or('actions', $kindly->dictOf($s), []); + + $avatar = $DIC["user.avatar.factory"]->avatar('xsmall'); + $avatar->setUsrId(ANONYMOUS_USER_ID); + $avatar->setName(ilStr::subStr($username, 0, 2)); + $public_image = $avatar->getUrl(); + $item = $this->uiFactory->item()->standard($username)->withLeadImage($this->uiFactory->image()->standard( + $public_image, + 'Profile image of ' . $username + )); + + if (ilChatroom::checkPermissionsOfUser($user_id, 'moderate', $ref_id)) { + $item = $item->withProperties([ + $this->ilLng->txt('role') => $this->ilLng->txt('il_chat_moderator'), + ]); + } + $item = $item->withActions($this->uiFactory->dropdown()->standard($this->buildUserActions($user_id, $actions))); + + + $this->sendResponse($this->uiRenderer->renderAsync($item), 'text/html'); + } + + /** + * @param array $actions + * @return array' - + '' - + '', - ); - - if (typeof options.hide !== 'undefined' && options.hide == true) { - line.addClass('hidden_entry'); - } - - line.data('ilChatList', options); - - const $this = $(this); - $this.data('ilChatList')._index.id_0 = line; - - line.find('button').on('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - menuContainer.find('li').remove(); - - const data = line.data('ilChatList'); - - $.each($this.data('ilChatList')._menuitems, function () { - if (this.permission == undefined) { - menuContainer.append(getMenuLine(this.label, this.callback)); - } else if ( - il.Chatroom.getUserInfo().moderator && inArray(this.permission, 'moderator') >= 0 - || il.Chatroom.getUserInfo().id == data.owner && inArray(this.permission, 'owner') >= 0 - ) { - menuContainer.append(getMenuLine(this.label, this.callback)); - } - }); - - menuContainer.appendTo(line.find('.btn-group')); - - menuContainer.data('ilChat', { - context: line.data('ilChatList'), - }); - - menuContainer.show(); - }); - - if (options.type == 'room' && options.owner == il.Chatroom.getUserInfo().id) { - line.addClass('self'); - } - - $(this).append(line); - if (line.hasClass('hidden_entry')) { - line.hide(); - } - - return $(this).ilChatList('sort'); - }, - sort() { - const tmp = []; - $.each($(this).data('ilChatList')._index, function (i) { - tmp.push({ id: i, data: this }); - }); - - tmp.sort((a, b) => ((a.data.data('ilChatList').label < b.data.data('ilChatList').label) ? -1 : 1)); - for (let i = 0; i < tmp.length; ++i) { - $(this).append(tmp[i].data); - } - - return $(this); - }, - removeById(id) { - const line = $(this).data('ilChatList')._index[`id_${id}`]; - if (line) { - const data = line.data('ilChatList'); - // line.remove(); - if (data.type == '') { - $(`${data.type}_${id}`).remove(); - } - delete $(this).data('ilChatList')._index[`id_${id}`]; - } - return $(this); - }, - getDataById(id) { - return $(this).data('ilChatList')._index[`id_${id}`] ? $(this).data('ilChatList')._index[`id_${id}`].data('ilChatList') : undefined; - }, - setNewEvents(newEvents) { - const data = $(this).data('ilChatList')._index.id_0.data('ilChatList'); - if (data) { - data.new_events = newEvents; - } - }, - getAll() { - const result = []; - $.each($(this).data('ilChatList')._index, function () { - result.push(this.data('ilChatList')); - }); - - result.sort((a, b) => ((a.label < b.label) ? -1 : 1)); - - return result; - }, - clear() { - menuContainer.html(''); - $(this).data('ilChatList', { - _index: {}, - _menuitems: $(this).data('ilChatList')._menuitems, - }); - }, - }; - - if (methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); - } - $.error(`Method ${method} does not exist on jQuery.ilChatList`); - }; - - const lastHandledDate = {}; - $.fn.ilChatMessageArea = function (method) { - const scrollChatArea = function (container, state) { - if (state.scrolling) { - $(container).parent().animate({ - scrollTop: $(container).height(), - }, 5); - } - }; - - const methods = { - init(s) { - $(this).data('ilChatMessageArea', { - _scopes: {}, - _typeInfos: {}, - _state: s, - }); - - $(this).data('state', s); - }, - addScope(scope) { - const tmp = $('
'); - $(this).data('ilChatMessageArea')._scopes.id_0 = tmp; - $(this).append(tmp); - tmp.data('ilChatMessageArea', scope); - tmp.hide(); - - const fader = $('
'); - $(this).data('ilChatMessageArea')._typeInfos.id_0 = fader; - $(this).append(fader); - fader.data('ilChatMessageArea', scope); - fader.append($('
')); - fader.hide(); - }, - addTypingInfo(messageObject, text) { - let containers; - - containers = [$(this).data('ilChatMessageArea')._typeInfos.id_0]; - - $.each(containers, function () { - const container = this; - - if (!container || container == window) { - return; - } - - container.find('.typing-info').text(text); - }); - }, - addMessage(message) { - let containers; const - msgArea = $(this); - containers = [$(this).data('ilChatMessageArea')._scopes.id_0]; - - $.each(containers, function () { - const container = this; - - if (!container || container == window) { - return; - } - - const line = $('
') - .addClass((message.target != undefined && !message.target.public) ? 'private' : 'public'); - - switch (message.type) { - case 'message': - var { content } = message; - - if (message.from == undefined) { - const legacyMessage = JSON.parse(message.message); - content = legacyMessage.content; - message.format = legacyMessage.format; - message.from = message.user; - - if (message.timestamp.toString().length > 13) { // Max 32-Bit Integer. - message.timestamp = parseInt(message.timestamp.toString().substring(0, 13)); - } - } - - var messageDate = new Date(message.timestamp); - - if (typeof lastHandledDate.scope === 'undefined' - || lastHandledDate.scope == null - || lastHandledDate.scope.getDate() != messageDate.getDate() - || lastHandledDate.scope.getMonth() != messageDate.getMonth() - || lastHandledDate.scope.getFullYear() != messageDate.getFullYear()) { - container.append($(``)); - } - lastHandledDate.scope = messageDate; - - line.append($('').append(`${il.Chatroom.formatISOTime(message.timestamp)}, `)) - .append($('').append(message.from.username)); - - if (message.target) { - if (message.target.username != '') { - line.append($('@').append(message.target.username)); - } else { - line.append($('@').append('unknown')); - } - } - - var messageSpan = $(''); - messageSpan.html(content); - line.append($(':')) - .append(messageSpan); - - $('.room_0').addClass('new_events'); - - break; - case 'connected': - if (message.login || (message.users[0] && message.users[0].login)) { - line.append($('').append(il.Chatroom.translate('connect', { username: message.users[0].login }))); - line.addClass('notice'); - if (!msgArea.data('state').show_auto_msg) { - line.addClass('ilNoDisplay'); - } - } - break; - case 'disconnected': - if (message.login || (message.users[0] && message.users[0].login)) { - line.append($('').append(il.Chatroom.translate('disconnected', { username: message.users[0].login }))); - line.addClass('notice'); - if (!msgArea.data('state').show_auto_msg) { - line.addClass('ilNoDisplay'); - } - } - break; - case 'private_room_entered': - if (message.login || (message.users[0] && message.users[0].login)) { - line - .append($('').append(`${il.Chatroom.formatISOTime(message.timestamp)}, `)) - .append($('').append(message.login || message.users[0].login)) - .append($(':')) - .append($('').append(il.Chatroom.translate('connect', { username: message.users[0].login }))); - } - break; - case 'private_room_left': - case 'notice': - line.append($('').append(message.content)); - line.addClass('notice'); - if (!msgArea.data('state').show_auto_msg) { - line.addClass('ilNoDisplay'); - } - break; - case 'error': - line.append($('').append(message.content)); - line.addClass('error'); - break; - case 'userjustkicked': - break; - } - - container.append(line); - scrollChatArea(container, msgArea.data('state')); - }); - - return $(this); - }, - hasContent(id) { - return $(this).data('ilChatMessageArea')._scopes[`id_${id}`].find('div').length > 0; - }, - clearMessages() { - $(this).data('ilChatMessageArea')._scopes.id_0.find('div').html(''); - }, - show(posturl, leaveCallback) { - const scopes = $(this).data('ilChatMessageArea')._scopes; - const typeInfos = $(this).data('ilChatMessageArea')._typeInfos; - const msgArea = $(this); - - $.each(scopes, function () { - $(this).attr('aria-live', 'off').hide(); - }); - - $.each(typeInfos, function () { - $(this).hide().find('[aria-live]').attr('aria-live', 'off'); - }); - - scopes.id_0.attr('aria-live', 'polite').show(); - typeInfos.id_0.show().find('[aria-live]').attr('aria-live', 'polite'); - - scrollChatArea(scopes.id_0, msgArea.data('state')); - $('.current_room_title').text(scopes.id_0.data('ilChatMessageArea').title); - - $('.in_room').removeClass('in_room'); - - $('.room_0').addClass('in_room'); - - $('#chat_users').find('.online_user').not('.hidden_entry').show(); - - if ($('.online_user:visible').length == 0) { - $('.no_users').show(); - } else { - $('.no_users').hide(); - } - - msgArea - .off('auto-message:toggle') - .off('msg-scrolling:toggle') - .on('auto-message:toggle', (e, isActive, url) => { - const state = msgArea.data('state'); - - let msgState = 1; - if (isActive) { - state.show_auto_msg = true; - $('#chat_messages .messageLine.notice').removeClass('ilNoDisplay'); - } else { - msgState = 0; - state.show_auto_msg = false; - $('#chat_messages .messageLine.notice').addClass('ilNoDisplay'); - } - - msgArea.data('state', state); - - $.ajax({ - type: 'POST', - url, - data: { state: msgState }, - }); - }) - .on('msg-scrolling:toggle', (e, isActive) => { - const state = msgArea.data('state'); - - if (isActive) { - state.scrolling = true; - } else { - state.scrolling = false; - } - }); - - return $(this); - }, - }; - - if (methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); - } - $.error(`Method ${method} does not exist on jQuery.ilChatMessageArea`); - }; -}(jQuery)); diff --git a/components/ILIAS/Chatroom/resources/iliaschat.jquery.js b/components/ILIAS/Chatroom/resources/iliaschat.jquery.js deleted file mode 100644 index c8e60a949a1d..000000000000 --- a/components/ILIAS/Chatroom/resources/iliaschat.jquery.js +++ /dev/null @@ -1,1608 +0,0 @@ -/** - * This file is part of ILIAS, a powerful learning management system - * published by ILIAS open source e-Learning e.V. - * - * ILIAS is licensed with the GPL-3.0, - * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with the - * source code, too. - * - * If this is not the case or you just want to try ILIAS, you'll find - * us at: - * https://www.ilias.de - * https://github.com/ILIAS-eLearning - * - *********************************************************************/ - -(function (root, scope, factory) { - scope.Chatroom = factory(root, root.jQuery); -}(window, il, function init(root, $) { - "use strict"; - - let room, - initial, - _scope, - personalUserInfo, - redirectUrl, - translation, - posturl, - serverConnector, - logger, - messageOptions, - iliasConnector, - chatActions; - - jQuery.fn.extend({ - insertAtCaret: function (value) { - return this.each(function (i) { - if (document.selection) { - //For browsers like Internet Explorer - this.focus(); - const sel = document.selection.createRange(); - sel.text = value; - this.focus(); - } else if (this.selectionStart || this.selectionStart == '0') { - //For browsers like Firefox and Webkit based - const startPos = this.selectionStart, - endPos = this.selectionEnd, - scrollTop = this.scrollTop; - - this.value = this.value.substring(0, startPos) + value + this.value.substring(endPos, this.value.length); - this.focus(); - this.selectionStart = startPos + value.length; - this.selectionEnd = startPos + value.length; - this.scrollTop = scrollTop; - } else { - this.value += value; - this.focus(); - } - }); - } - }); - - function closeMenus() { - $('.menu').hide(); - } - - function formatToTwoDigits(nr) { - nr = "" + nr; - while (nr.length < 2) { - nr = "0" + nr; - } - - return nr; - } - - function translate(key, args) { - return translation.translate(key, args); - } - - function formatISOTime(time) { - let format = translation.translate("timeformat"); - const date = new Date(time); - - format = format.replace(/H/, formatToTwoDigits(date.getHours())); - format = format.replace(/i/, formatToTwoDigits(date.getMinutes())); - format = format.replace(/s/, formatToTwoDigits(date.getSeconds())); - - return format; - } - - function formatISODate(time) { - let format = translation.translate("dateformat"); - const date = new Date(time); - - format = format.replace(/Y/, date.getFullYear()); - format = format.replace(/m/, formatToTwoDigits(date.getMonth() + 1)); - format = format.replace(/d/, formatToTwoDigits(date.getDate())); - - return format; - } - - function isIdInArray(id, objects) { - for (let i in objects) { - if (typeof objects[i] != 'undefined' && typeof objects[i].id != 'undefined' && objects[i].id == id) { - return true; - } - } - - return false; - } - - const Logger = function Logger() { - this.logServerResponse = function (message) { - _log('Server-Response', message); - }; - - this.logServerRequest = function (message) { - _log('Server-Request', message); - }; - - this.logILIASResponse = function (message) { - _log('ILIAS-Response', message); - }; - - this.logILIASRequest = function (message) { - _log('ILIAS-Request', message); - }; - - function _log(type, message) { - console.log(type, message); - } -}; - - /** - * This class translates all translation key to language text. - * It is only able to translate thouse keys, which are delivered into _lang via the constructor - * @param {array} _lang - * @constructor - */ - const Translation = function Translation(_lang) { - /** - * - * @param {string} key - * @param {object} args - * @returns {string} - */ - this.translate = function (key, args) { - if (_lang[key]) { - let lng = _lang[key]; - - if (args !== undefined) { - for (let i in args) { - lng = lng.replace(new RegExp('#' + i + '#', 'g'), args[i]); - } - } - - return lng; - } - - return '#' + key + '#'; - } - }; - - const ProfileImageLoader = function (_userId, _onFinishCallback) { - const requestUserImages = function (userIds) { - let difference = []; - - $.grep(userIds, function (el) { - if ($.inArray(el, Object.keys(ProfileImageLoader._imagesByUserId)) === -1) { - difference.push(el); - } - }); - - if (difference.length > 0) { - $.get( - initial.profile_image_url + '&usr_ids=' + userIds.join(','), - function (response) { - $.each(response, function (id, item) { - const img = new Image(); - img.src = item.profile_image; - - ProfileImageLoader._imagesByUserId[id] = img; - }); - _onFinishCallback() - }, - 'json' - ); - } else { - _onFinishCallback() - } - }; - - requestUserImages(_userId); - - this.getProfileImage = function (_userId) { - if (ProfileImageLoader._imagesByUserId.hasOwnProperty(_userId)) { - return ProfileImageLoader._imagesByUserId[_userId]; - } - - const img = new Image(); - img.src = initial.no_profile_image_url; - - return img; - }; - }; - ProfileImageLoader._imagesByUserId = {}; - - /** - * This class renders the chat gui and manages all gui actions. - * - * @param {Translation} _translation - * @constructor - */ - const GUI = function GUI(_translation) { - let _prevSize = {width: 0, height: 0}, - _$anchor = $('#chat_messages'); - - this.renderHeaderAndActionButton = function () { - $('
') - .append($('#chat_actions_wrapper')) - .insertBefore($('.il_HeaderInner').find('h1')); - }; - - /** - * @param {number} interval - */ - this.resizeChatWindowInInterval = function (interval) { - window.setInterval(function () { - const body = $('body'), - currentSize = {width: body.width(), height: body.height()}; - - if (currentSize.width != _prevSize.width || currentSize.height != _prevSize.height) { - $('#chat_sidebar_wrapper').height( - $('#chat_sidebar').parent().height() - $('#chat_sidebar_tabs').height() - ); - _prevSize = {width: body.width(), height: body.height()}; - } - }, interval); - }; - - this.initChatMessageArea = function (state) { - _$anchor.ilChatMessageArea(state); - }; - - this.addChatMessageArea = function (title, owner) { - _$anchor.ilChatMessageArea('addScope', { - title: title, - owner: owner - }); - }; - - this.showChatMessageArea = function (scope) { - _$anchor.ilChatMessageArea('show', scope); - }; - - this.addMessage = function (messageObject) { - _$anchor.ilChatMessageArea('addMessage', messageObject); - }; - - this.addTypingInfo = function(messageObject, text) { - _$anchor.ilChatMessageArea('addTypingInfo', messageObject, text); - }; -}; - - /** - * This class manages all users for all rooms. - * - * @constructor - */ - const UserManager = function UserManager() { - - /** - * Stores all users for this room. - * - * @type {Array} - * @private - */ - let _usersByRoom = []; - - /** - * Adds a user to the delivered room. - * - * @param {JSON} userdata - * @returns {boolean} - */ - this.add = function (userdata) { - for (let i in _usersByRoom) { - const current = _usersByRoom[i]; - if (current.id == userdata.id) { - _usersByRoom[i].label = userdata.label; - return false; - } - } - - _usersByRoom.push(userdata); - - return true; - }; - - /** - * Remove a delivered user from the delivered room. - * - * @param {number} userId - */ - this.remove = function (userId) { - for (let i in _usersByRoom) { - const user = _usersByRoom[i]; - if (user.id == userId) { - delete _usersByRoom[i]; - } - } - }; - - /** - * Get all users of this room. - * - * @returns {Array} - */ - this.getUsersInRoom = function () { - return _usersByRoom; - }; - - /** - * Removes all users from the delivered room. - * - * @param {string} roomId - */ - this.clear = function () { - _usersByRoom = []; - }; - }; - - /** - * This class renders all available action for a user in a chat room. - * - * @param {string} selector - * @param {Closure} txt - * @param {ILIASConnector} _connector - * @constructor - */ - const ChatUsers = function ChatUsers(selector, txt, _connector) { - - let _$anchor = $(selector); - - /** - * Initializes all available action. - */ - this.init = function () { - const actions = []; - - actions.push(_addAddressAction()); - actions.push(_addWhisperAction()); - - if (personalUserInfo.moderator == true) { - actions.push(_addKickAction()); - } - if (personalUserInfo.moderator == true) { - actions.push(_addBanAction()); - } - _$anchor.ilChatUserList(actions); - }; - - /** - * Adds an action to address a chat message to the selected user - * - * @returns {{label: string, callback: callback}} - * @private - */ - function _addAddressAction() { - return { - label: txt('address'), - callback: function () { - setRecipientOptions(this.id, 1); // @TODO setRecipientOptions(this.id, 1); - } - }; - } - - /** - * Adds an action to send a private message to one user in the current room. - * - * @returns {{label: string, callback: callback}} - * @private - */ - function _addWhisperAction() { - return { - label: txt('whisper'), - callback: function () { - setRecipientOptions(this.id, 0); // @TODO setRecipientOptions(this.id, 0); - } - }; - } - - /** - * Adds an action to kick a user from the current room. - * - * @returns {{label: string, callback: callback, permission: string[]}} - * @private - */ - function _addKickAction() { - return { - label: txt('kick'), - callback: function () { - confirmModal(txt('kick'), txt('kick_question')).then(function(x){ - if (x) { - _connector.kick(this.id); - } - }.bind(this)); - }, - permission: ['moderator', 'owner'] - }; - } - - /** - * Adds an action to ban a user from the current room. - * - * @returns {{label: string, callback: callback, permission: string[]}} - * @private - */ - function _addBanAction() { - return { - label: txt('ban'), - callback: function () { - confirmModal(txt('ban'), txt('ban_question')).then(function(x){ - if (x) { - _connector.ban(this.id); - } - }.bind(this)); - }, - permission: ['moderator'] - }; - } - }; - - /** - * This class renders all available actions for the chat into a dropdown. - * - * @param {string} selector - * @param {Closure} txt - * @param {ILIASConnector} _connector - * @oaram {UserManager} userManager - */ - const ChatActions = function (selector, txt, _connector, userManager) { - - let _$anchor = $(selector), _menuEntries = []; - - // Done - /** - * Setup ChatActions - */ - this.init = function () { - _resetEntries(); - - $(this).removeClass('chat_new_events'); - _addInviteSubRoomAction(); - _addClearHistoryAction(); - - const box = document.querySelector('#chat_function_list'); - const createButton = function (entry) { - const node = document.createElement('button'); - node.style.display = 'block'; - node.style.margin = '0 0 5px 0'; - node.classList.add('btn-link'); - node.classList.add('btn'); - node.textContent = entry.label; - node.addEventListener('click', entry.callback); - - return node; - }; - _menuEntries.map(createButton).forEach(box.appendChild.bind(box)); - }; - - /** - * Clears all menu entries - * - * @private - */ - function _resetEntries() { - _menuEntries = []; - } - - /** - * Adds an action to invite a user to a sub room. - * Invitation can be done by user id or user login name. - * Users are able to invite users subscribed to the main room or search for existing users in ILIAS. - * - * @TODO Check if user is in subRoom and has permission to invite users to private room. - * - * @private - */ - function _addInviteSubRoomAction() { - _menuEntries.push( - { - label: txt('invite_users'), - callback: inviteUserToRoomDialog - } - ); - return room; - } - - - const searchForUsers = function(search){ - const call = m => o => o[m](); - return fetch(posturl.replace(/postMessage/, 'inviteUsersToPrivateRoom-getUserList') + '&q=' + search).then(call('json')).then(function(response){ - const usersInRoom = userManager.getUsersInRoom(); - return response.items.filter(function(x){ - return !isIdInArray(x.id, usersInRoom); - }); - }); - }; - - const createUserResults = function(loading, noUser){ - let current = loading; - loading.classList.add('ilNoDisplayChat'); - noUser.classList.add('ilNoDisplayChat'); - - const hideCurrent = function(){ - current.classList.add('ilNoDisplayChat'); - }; - - const change = function(newOne){ - hideCurrent(); - current = newOne; - current.classList.remove('ilNoDisplayChat'); - }; - - return { - loaded: function(then){ - return function(items){ - items.length ? hideCurrent() : change(noUser); - return then(items); - }; - }, - loading: function(){ - change(loading); - current = loading; - }, - noRequest: hideCurrent, - }; - }; - - const inviteUserToRoomDialog = function(){ - const input = document.querySelector('#invite_user_text'); - const userResults = createUserResults( - document.querySelector('#invite_users_loading'), - document.querySelector('#invite_users_no_user') - ); - const body = document.querySelector('#invite_users_container'); - input.value = ''; - const modal = window.il.Modal.dialogue({ - header: txt('invite_users'), - show: true, - body: body, - onShown: function(){ - body.classList.remove('ilNoDisplayChat'); - } - }); - - /* Please not that an empty input will not trigger a search so the last message will not be removed on empty input. */ - $(input).autocomplete({ - appendTo: input.parentNode, - source: function(request, response) { - searchForUsers(request.term).then(userResults.loaded(response)); - }, - search: function() { - if (this.value.length > 2) { - userResults.loading(); - return true; - } - userResults.noRequest(); - return false; - }, - select: function(event, ui){ - _connector.inviteToPrivateRoom(ui.item.id, 'byId'); - modal.hide(); - }, - }); - }; - - /** - * Adds an action to clear the current room chat history. - * - * @TODO Check if user has permission to clear history - * - * @private - */ - function _addClearHistoryAction() { - if (personalUserInfo.moderator) { - const label = txt('clear_room_history'); - _menuEntries.push({ - label: label, - callback: function () { - confirmModal(label, txt('clear_room_history_question'), txt('delete')).then(function(x){ - if (x) { - _connector.clear(); - } - }); - } - }); - } - } - - /** - * Renders an html string to show users which are able to be invited to the sub room. - * - * @param {string} userValue - * @param {string} invitationType - * @param {string} label - * @private - */ - function _addUserForInvitation(userValue, invitationType, label) { - const link = $('') - .prop('href', '#') - .text(label) - .click(function (e) { - e.preventDefault(); - e.stopPropagation(); - _connector.inviteToPrivateRoom(userValue, invitationType); - }); - const line = $('
  • ') - .addClass('invite_user_line_id') - .addClass('invite_user_line') - .append(link); - - $('#invite_users_available').append(line); - } - }; - -const ChatTypingUsersTextGeneratorFactory = (function () { - let instances = {}; - - /** - * - * @param {String} conversationId - * @constructor - */ - function TypingUsersTextGenerator(conversationId) { - this.conversationId = conversationId; - this.typingMap = new Map(); - } - - /** - * - * @param {Number} id - * @param {String} username - */ - TypingUsersTextGenerator.prototype.addTypingSubscriber = function(id, username) { - if (!this.typingMap.has(id)) { - this.typingMap.set(id, username); - } - } - - /** - * - * @param {Number} id - * @param {String} username - */ - TypingUsersTextGenerator.prototype.removeTypingSubscriber = function(id, username) { - if (this.typingMap.has(id)) { - this.typingMap.delete(id); - } - }; - - /** - * - * @param {il.Language} language - * @returns {string} - */ - TypingUsersTextGenerator.prototype.text = function ( language) { - const names = Array.from(this.typingMap.values()); - - if (names.length === 0) { - return ''; - } else if (1 === names.length) { - return language.txt("chat_user_x_is_typing", names[0]); - } - - return language.txt("chat_users_are_typing"); - }; - - /** - * - * @param {String} conversationId - * @returns {TypingUsersTextGenerator} - */ - function createInstance(conversationId) { - return new TypingUsersTextGenerator(conversationId); - } - - return { - /** - * @param {String} conversationId - * @returns {TypingUsersTextGenerator} - */ - getInstance: function (conversationId) { - if (!instances.hasOwnProperty(conversationId)) { - instances[conversationId] = createInstance(conversationId); - } - return instances[conversationId]; - } - }; -})(); - -const ChatTypingBroadcasterFactory = (function () { - let instances = {}, ms = 5000; - - /** - * - * @param {Function} onTypingStarted - * @param {Function} onTypeingStopped - * @constructor - */ - function TypingBroadcaster(onTypingStarted, onTypingStopped) { - this.is_typing = false; - this.timer = 0; - this.onTypingStarted = onTypingStarted; - this.onTypingStopped = onTypingStopped; - } - - TypingBroadcaster.prototype.release = function() { - if (this.is_typing) { - window.clearTimeout(this.timer); - this.onTimeout(); - } - } - - TypingBroadcaster.prototype.onTimeout = function() { - window.clearTimeout(this.timer); - this.is_typing = false; - this.onTypingStopped.call(); - }; - - TypingBroadcaster.prototype.registerTyping = function() { - if (this.is_typing) { - window.clearTimeout(this.timer); - this.timer = window.setTimeout(this.onTimeout.bind(this), ms); - } else { - this.is_typing = true; - this.onTypingStarted.call(); - this.timer = window.setTimeout(this.onTimeout.bind(this), ms); - } - }; - - /** - * - * @param {String} scopeId - * @param {Function} onTypingStarted - * @param {Function} onTypingStopped - * @returns {TypingBroadcaster} - */ - function createInstance(scopeId, onTypingStarted, onTypingStopped) { - return new TypingBroadcaster(onTypingStarted, onTypingStopped); - } - - return { - /** - * @param {String} scopeId - * @param {Function} onTypingStarted - * @param {Function} onTypingStopped - * @returns {TypingBroadcaster} - */ - getInstance: function (scopeId, onTypingStarted, onTypingStopped) { - if (!instances.hasOwnProperty(scopeId)) { - instances[scopeId] = createInstance(scopeId, onTypingStarted, onTypingStopped); - } - return instances[scopeId]; - }, - releaseAll: function () { - for (let conversationId in instances) { - if (instances.hasOwnProperty(conversationId)) { - instances[conversationId].release(); - } - } - } - }; -})(); - -/** - * This class handles responses of all asynchronous request done by ILIASConnector. - * It has to be passed to the ILIASConnector instance. - * - * @constructor - */ -var ILIASResponseHandler = function ILIASResponseHandler() { - /** - * Handles the response of a leavePrivateRoom request. - * Shows the related ilChatMessageArea - * - * @param {{}} response - */ - this.leavePrivateRoom = function (response) { - if (!_validate(response)) { - return; - } - - $('#chat_messages').ilChatMessageArea('show'); - }; - - /** - * Handles the response of an inviteToPrivateRoom request. - * It closes the invitation dialog. - * - * @param {{}} response - */ - this.inviteToPrivateRoom = function (response) { - if (!_validate(response)) { - return; - } - - // $('#invite_users_container').ilChatDialog('close'); - }; - - /** - * Default handler for an ILIAS request. It just validates the response. - * - * @param {{}} response - * - * @return {boolean} - */ - this.default = function (response) { - logger.logILIASResponse('default'); - return _validate(response); - }; - - /** - * Checks if the request was successfully. Unless it displays an error message as alert. - * - * @param {{success: boolean, reason: string}} response - * @returns {boolean} - * @private - */ - function _validate(response) { - if (!response.success) { - alert(response.reason); - return false; - } - return true; - } - }; - - /** - * This class connects the client to the related ILIAS environment. Communication is handled by sending - * JSON requests. The response of each request is handled through callbacks delivered by the ILIASResponseHandler - * which is passed through the constructor - * - * @param {string} _postUrl - * @param {ILIASResponseHandler} responseHandler - * @constructor - */ - const ILIASConnector = function (_postUrl, responseHandler) { - - let _self = this; - - /** - * Sends a heartbeat to ILIAS in a delivered interval. It is used to keep the session for an ILIAS user open. - * - * @param {number} interval - */ - this.heartbeatInterval = function (interval) { - window.setInterval(function () { - _sendRequest('poll', {}, function(response) {}); - }, interval); - }; - - /** - * Sends a request to ILIAS to leave a private room. - */ - this.leavePrivateRoom = function () { - logger.logILIASRequest('leavePrivateRoom'); - _sendRequest('privateRoom-leave', {}, responseHandler.leavePrivateRoom); - }; - - /** - * Sends a request to ILIAS to invite a specific user to a private room. - * The invitation can be done by two types - * 1. byId - * 2. byLogin - * - * @param {string} userValue - * @param {string} invitationType - */ - this.inviteToPrivateRoom = function (userValue, invitationType) { - _sendRequest('inviteUsersToPrivateRoom-' + invitationType, { - user: userValue - }, responseHandler.inviteToPrivateRoom); - }; - - /** - * Sends a request to ILIAS to clear the chat history - */ - this.clear = function () { - _sendRequest('clear', {}, responseHandler.default); - }; - - /** - * Sends a request to ILIAS to kick a user from a specific room. The room can either be a private or the main room. - * - * @param {number} userId - */ - this.kick = function (userId) { - _sendRequest('kick', {user: userId}, responseHandler.default); - }; - - /** - * Sends a request to ILIAS to ban a user from a specific room. The room can either be a private or the main room. - * - * @param {number} userId - */ - this.ban = function (userId) { - _sendRequest('ban-active', {user: userId}, responseHandler.default); - }; - - /** - * Sends a asynchronously JSON request to ILIAS. - * - * @param {string} action - * @param {{}} params - * @param {function} responseCallback - * @private - */ - function _sendRequest(action, params, responseCallback) { - $.get(_postUrl.replace(/postMessage/, action) + _generateParamsString(params), function (response) { - response = $.getAsObject(response); - responseCallback(response); - }, 'JSON'); - } - - /** - * Generates request parameter string for an asynchronous request. - * - * @param {Array} params - * @returns {string} - * @private - */ - function _generateParamsString(params) { - let string = ''; - for (let key in params) { - string += '&' + key + '=' + encodeURIComponent(params[key]); - } - return string; - } - }; - - /** - * This class connects the client to the related chat server. Communication is handled through websockets as far as - * it is supported by the users browser. Otherwise it uses polling method to communicate. Messages are send through - * `socket.emit`. Messages are received through `socket.on`. There can be 3 types of messages. - * 1. Text messages which are send by the chat users - * 2. Notification messages. This are informational messages which are triggered by the system. - * 3. Action messages. This messages triggers action which have to be executed in the client. This messages are - * triggered by the System. - * - * @param url - * @param scope - * @param user - * @param {UserManager} userManager - * @param {GUI} gui - * @constructor - */ - const ServerConnector = function ServerConnector(url, scope, user, userManager, gui, subdirectory) { - - let _socket; - - /** - * Setup server connector - */ - this.init = function () { - _socket = io.connect(url, {path: subdirectory}); - - _socket.on('message', _onMessage); - _socket.on('connect', function(){ - _socket.emit('login', user.login, user.id); - }); - _socket.on('user_invited', _onUserInvited); - _socket.on('private_room_entered', _onPrivateRoomEntered); - _socket.on('connected', _onConnected); - _socket.on('userjustkicked', _onUserKicked); - _socket.on('userjustbanned', _onUserBanned); - _socket.on('clear', _onClear); - _socket.on('notice', _onNotice); - _socket.on('userStartedTyping', _onUserStartedTyping); - _socket.on('userStoppedTyping', _onUserStoppedTyping); - _socket.on('userlist', _onUserlist); - _socket.on('shutdown', function(){ - _socket.removeAllListeners(); - _socket.close(); - window.location.href = redirectUrl; - }); - - $(window).on('beforeunload',function() { - ChatTypingBroadcasterFactory.releaseAll(); - _socket.close(); - }); - - _initSubmit(); - }; - - /** - * Sends enter room to server - * - * @param {number} roomId - */ - this.enterRoom = function (roomId) { - logger.logServerRequest('enterRoom'); - _socket.emit('enterRoom', roomId); - }; - - /** - * @param {Function} callback - */ - this.onLoggedIn = function (callback) { - _socket.on('loggedIn', function () { - callback(); - }); - }; - - this.userStartedTyping = function(roomId) { - logger.logServerRequest('userStartedTyping'); - _socket.emit('userStartedTyping', roomId); - } - - this.userStoppedTyping = function(roomId) { - logger.logServerRequest('userStoppedTyping'); - _socket.emit('userStoppedTyping', roomId); - } - - /** - * Displays chatmessage in chat - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * from: {id: number, name: string}, - * format: {style: string, color: string, family: string, size: string} - * }} messageObject - * - * @private - */ - function _onMessage(messageObject) { - gui.addMessage(messageObject); - } - - /** - * Adds chat for user invitation - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * title: string - * owner: number - * }} messageObject - * - * @private - */ - function _onUserInvited(messageObject) { - gui.addChatMessageArea(messageObject.title, messageObject.owner); - } - - /** - * Enters a private Room - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * title: string, - * owner: number, - * subscriber: {id: number, username: string}, - * usersInRoom: {Array} - * }} messageObject - * - * @private - */ - function _onPrivateRoomEntered(messageObject) { - logger.logServerResponse('onPrivateRoomEntered'); - } - - function _onConnected(messageObject) { - let loader = new ProfileImageLoader($.map(messageObject.users, function (val) { - return val.id; - }), function () { - $(messageObject.users).each(function (i) { - let data = { - id: this.id, - label: this.login, - type: 'user', - image: loader.getProfileImage(this.id) - }; - $('#chat_users').ilChatUserList('add', data); - - userManager.add(data); - - $('#chat_messages').ilChatMessageArea('addMessage', { - login: data.label, - timestamp: messageObject.timestamp, - type: 'connected' - }); - }); - }); - } - - /** - * Kicks a user from chat - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * }} messageObject - * - * @private - */ - function _onUserKicked(messageObject) { - logger.logServerResponse('onUserKicked'); - - userManager.remove(user.id); - - // If user is kicked from sub room, redirect to main room - - $('#chat_users').ilChatUserList('removeById', user.id); - window.location.href = redirectUrl + "&msg=kicked"; - } - - /** - * Banns a user from chat - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * }} messageObject - * - * @private - */ - function _onUserBanned(messageObject) { - if (_socket) { - _socket.removeAllListeners(); - _socket.close(); - } - window.location.href = redirectUrl + "&msg=banned"; - } - - /** - * Clears chat history - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * }} messageObject - * - * @private - */ - function _onClear(messageObject) { - $('#chat_messages').ilChatMessageArea('clearMessages'); - } - - /** - * Adds a notice to chat - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * data: {} - * }} messageObject - * - * @private - */ - function _onNotice(messageObject) { - messageObject.content = translation.translate(messageObject.content, messageObject.data); - - gui.addMessage(messageObject); - } - - /** - * Updates the list of users. - * - * @param {{ - * type:string, - * timestamp: number, - * content: string, - * roomId: number, - * users: {} - * }} messageObject - * - * @private - */ - function _onUserlist(messageObject) { - let users = messageObject.users; - - logger.logServerResponse("onUserlist"); - - let loader = new ProfileImageLoader($.map(users, function (val) { - return val.id; - }), function () { - Object.values(users).forEach(function(otherUser){ - const chatUser = { - id: otherUser.id, - label: otherUser.username, - type: 'user', - hide: otherUser.id == user.id, - image: loader.getProfileImage(otherUser.id) - }; - - userManager.add(chatUser); - - $('#chat_users').ilChatUserList('add', chatUser, chatUser.id, {hide: chatUser.hide}); - - if (chatUser.id != user.id) { - $('.user_' + chatUser.id).show(); - } else { - $('.user_' + chatUser.id).hide(); - } - }); - - // remove old users - const currentUsersInRoom = userManager.getUsersInRoom(); - - for (let key in currentUsersInRoom) { - const userId = currentUsersInRoom[key].id; - if (!isIdInArray(userId, users)) { - userManager.remove(userId); - $('#chat_users').ilChatUserList('removeById', userId); - } - } - - if ($('.online_user:visible').length == 0) { - $('.no_users').show(); - } else { - $('.no_users').hide(); - } - }); - } - - function _onUserStartedTyping(message) { - logger.logServerResponse("onUserStartedTyping"); - - const subscriber = JSON.parse(message.subscriber), - scope = message.roomId + '_0', - generator = ChatTypingUsersTextGeneratorFactory.getInstance(scope); - - generator.addTypingSubscriber(subscriber.id, subscriber.username); - - gui.addTypingInfo(message, generator.text( - il.Language - )); - } - - function _onUserStoppedTyping(message) { - logger.logServerResponse("onUserStoppedTyping"); - - const subscriber = JSON.parse(message.subscriber), - scope = message.roomId + '_0', - generator = ChatTypingUsersTextGeneratorFactory.getInstance(scope); - - generator.removeTypingSubscriber(subscriber.id, subscriber.username); - - gui.addTypingInfo(message, generator.text( - il.Language - )); - } - - /** - * Setup message submit to server - * - * @private - */ - function _initSubmit() { - $('#submit_message').click(function(e) { - e.preventDefault(); - e.stopPropagation(); - _sendMessage(); - }); - - // when the client hits ENTER on their keyboard - $('#submit_message_text').keydown(function (e) { - const keycode = e.keyCode || e.which; - - if (keycode === 13 && !e.shiftKey) { - e.preventDefault(); - e.stopPropagation(); - - $(this).blur(); - _sendMessage(); - } - }); - - $('#submit_message_text').keyup(function(e) { - if (personalUserInfo.broadcast_typing !== true) { - return; - } - - const room_id = _scope; - - const broadcaster = ChatTypingBroadcasterFactory.getInstance( - room_id + '_0', - function() { - serverConnector.userStartedTyping(room_id); - }, - function() { - serverConnector.userStoppedTyping(room_id); - } - ); - - const keycode = e.keyCode || e.which; - if (keycode === 13) { - broadcaster.release(); - return; - } - - broadcaster.registerTyping(); - }); - } - - /** - * Sent message to server - * - * @private - */ - function _sendMessage() { - const $textInput = $('#submit_message_text'), - content = $textInput.val(); - - if (content.trim() !== '') { - const message = { - content: content, - format: {} - }; - - if (messageOptions['recipient'] != undefined && messageOptions['recipient'] != false) { - message.target = { - username: $('#chat_users').ilChatUserList('getDataById', messageOptions['recipient']).label, - id: messageOptions['recipient'], - public: messageOptions['public'] - } - } - - $textInput.val(''); - - if (personalUserInfo.broadcast_typing === true) { - const room_id = _scope; - - const broadcaster = ChatTypingBroadcasterFactory.getInstance( - room_id + '_0', - function() { - serverConnector.userStartedTyping(room_id); - }, - function() { - serverConnector.userStoppedTyping(room_id); - } - ); - - broadcaster.release(); - } - - _socket.emit('message', message, scope); - $textInput.focus(); - } - } -}; - - // Set recipient targeted/whispered - function setRecipientOptions(recipient, isPublic) { - messageOptions['recipient'] = recipient; - messageOptions['public'] = isPublic; - - $('#message_recipient_info').children().remove(); - if (recipient) { - messageOptions['recipient_name'] = $('#chat_users').ilChatUserList('getDataById', recipient).label; - $('#message_recipient_info_all').hide(); - $('#message_recipient_info').html( - $('' + translation.translate(isPublic ? 'speak_to' : 'whisper_to', { - user: $('#chat_users').ilChatUserList('getDataById', recipient).label, - myname: personalUserInfo.name - }) + '') - .append( - $(' (' + translation.translate('end_whisper') + ')').click( - function () { - setRecipientOptions(false, 1); - } - ) - ) - ).show(); - } else { - messageOptions['recipient_name'] = null; - $('#message_recipient_info_all').show(); - $('#message_recipient_info').hide(); - } - } - - $.getAsObject = function (data) { - if (typeof data == 'object') { - return data; - } - try { - return JSON.parse(data); - } catch (e) { - if (typeof console != 'undefined') { - console.log(e); - return {success: false}; - } - } - }; - - $.fn.chat = function (baseurl, instance) { - const gui = new GUI(translation); - const userManager = new UserManager(); - iliasConnector = new ILIASConnector(posturl, new ILIASResponseHandler()); - const chatUsers = new ChatUsers('#chat_users', translation.translate, iliasConnector); - - serverConnector = new ServerConnector(baseurl + '/' + instance, _scope, personalUserInfo, userManager, gui, initial.subdirectory); - chatActions = new ChatActions('#chat_actions', translation.translate, iliasConnector, userManager); - - //Setup Heartbeat to refresh the Session - iliasConnector.heartbeatInterval(120 * 1000); - serverConnector.init(); - serverConnector.onLoggedIn(function () { - serverConnector.enterRoom(_scope, 0); - - if (initial.enter_room) { - serverConnector.enterRoom(_scope, initial.enter_room); - } - - }); - - // Insert Chatheader into HTML next to AKTION-Button - gui.renderHeaderAndActionButton(); - // When private rooms are disabled, dont show chat header - // Resizes Chatwindow every 500 miliseconds - gui.resizeChatWindowInInterval(500); - // Initialize ChatMessageArea(); - gui.initChatMessageArea(initial.state); - gui.addChatMessageArea(translation.translate('main'), 0); - gui.showChatMessageArea(0); - - - // Initialize Chatlist user actions - chatUsers.init(); - // Initialize Chat Aktions Button - chatActions.init(); - - messageOptions = { - 'recipient': null, - 'recipient_name': null, - 'public': 1 - }; - - // @TODO DONO; - $('#enter_main').click(function (e) { - e.preventDefault(); - e.stopPropagation(); - $('#chat_messages').ilChatMessageArea('show'); - $('#chat_users').find('.online_user').not('.hidden_entry').show(); - }); - - //@TODO DONO - $('#tab_users').click(function (e) { - e.stopPropagation(); - e.preventDefault(); - closeMenus(); - $([$('#tab_users'), $('#tab_users').parent()]).each(function () { - this.removeClass('tabinactive').addClass('tabactive'); - }); - $([$('#tab_rooms'), $('#tab_rooms').parent()]).each(function () { - this.removeClass('tabactive').addClass('tabinactive'); - }); - - $('#chat_users').css('display', 'block'); - }); - $('#tab_users').click(); - - const loader = new ProfileImageLoader($.map(initial.users, function (val) { - return val.id; - }), function () { - $(initial.users).each(function () { - const tmp = { - id: this.id, - label: this.login, - type: 'user', - hide: this.id == personalUserInfo.id, - image: loader.getProfileImage(this.id) - }; - $('#chat_users').ilChatUserList('add', tmp, {hide: true}); - userManager.add(tmp); - }); - }); - - // Show initial messages - $(initial.messages).each(function () { - const message = this; - - message.timestamp = message.timestamp * 1000; - - if (message.type == 'notice') { - if (message.content == 'connect' && message.data.id == personalUserInfo.id) { - message.content = 'welcome_to_chat'; - } - message.content = translation.translate(message.content, message.data); - } - $('#chat_messages').ilChatMessageArea('addMessage', message); - }); - - // Build more options - function buildMoreOptions() { - const res = []; - for (let i in messageOptions) { - if (messageOptions[i] == null || messageOptions[i] == false) - continue; - res.push(i + '=' + encodeURIComponent(messageOptions[i])); - } - return res.join('&'); - } - - // Handle incomming Message - function handleMessage(message) { - const messageObject = (typeof message == 'object') ? message : $.getAsObject(message); - - //@TODO Debug anders realisieren - if (typeof DEBUG != 'undefined' && DEBUG) { - $('#chat_messages').ilChatMessageArea('addMessage', { - type: 'notice', - message: messageObject.type - }); - } - } - }; - - /** - * @param {string} label - * @param {string} message - * @param {undefined|string} buttonLabel - * @return {Promise} - */ - const confirmModal = function(label, message, buttonLabel){ - return new Promise(function(resolve){ - const body = document.createElement('div'); - body.textContent = message; - body.className = 'alert alert-warning'; - const header = document.createElement('div'); - header.classList.add('modal-title'); - header.textContent = label; - - const modal = il.Modal.dialogue({ - body: body, - header: header, - buttons: [ - { - type: 'button', - label: buttonLabel || label, - callback: function(){ - resolve(true); - resolve = function(){}; - modal.hide(); - } - }, - {type: 'button', label: translation.translate('cancel'), callback: function(){modal.hide();}}, - ], - onHide: function(){ - resolve(false); - } - }); - }); - }; - - let api = { - run: function(appDomElementId, lang, baseurl, instance, scope, postUrl, initialConfig) { - $("#submit_message_text").focus(); - - $(document).click(function() { - $(".dropdown-menu.menu").hide(); - }); - - _scope = scope; - room = scope; - initial = initialConfig; - initial.enter_room = initial.enter_room || 0; - personalUserInfo = initial.userinfo; - - redirectUrl = initial.redirect_url; - posturl = postUrl; - - logger = new Logger(); - translation = new Translation(lang); - - $("#" + appDomElementId).chat(baseurl, instance); - }, - leavePrivateRoom: function () { - iliasConnector.leavePrivateRoom(); - }, - getUserInfo: function() { - return personalUserInfo; - }, - formatISOTime: formatISOTime, - formatISODate: formatISODate, - translate: translate - }; - - return api; -})); diff --git a/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js b/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js new file mode 100644 index 000000000000..42017ec8b7c3 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/dist/Chatroom.min.js @@ -0,0 +1,15 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * 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)})(),L=e=>function(e){const t=k();return r.onArrived(t,e),t}((t=>t.addEventListener("click",e)));class S{#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.#L(),!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.#L(),!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,L((()=>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}#L(){this.#f.classList[this.#b.length?"add":"remove"]("ilNoDisplay")}}const C=(e,t)=>Object.keys(e).filter((e=>!Reflect.has(t,e))).map((t=>({key:t,value:e[t]})));class w{#S;#C;constructor(){this.#S={},this.#C=[]}find(e){return this.#S[e]}has(e){return Reflect.has(this.#S,String(e))}onChange(e){this.#C.push(e)}add(e,t){e=String(e),this.#S[e]=t,this.#w({added:[{key:e,value:t}],removed:[]})}remove(e){if(e=String(e),!Reflect.has(this.#S,e))return;const t=this.#S[e];delete this.#S[e],this.#w({added:[],removed:[{key:e,value:t}]})}setAll(e){const t={added:C(e,this.#S),removed:C(this.#S,e)};this.#S=e,this.#w(t)}all(){return this.#S}#w(e){this.#C.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;#M;#x;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.#M=_(["typing-info"]),this.#M.setAttribute("aria-live","polite"),this.#x=R,this.#O(),this.clearMessages(),this.#j()}addMessage(e){this.#x();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.innerHTML=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.innerHTML=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.innerHTML="",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.#x=e.remove.bind(e)}typingListChanged(){const e=Object.values(this.#E.all());0===e.length?this.#M.textContent="":1===e.length?this.#M.textContent=this.#_("chat_user_x_is_typing",e[0]):this.#M.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.#M)}#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 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.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(x("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 x=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 S(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)))}(S.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",M(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); diff --git a/components/ILIAS/Chatroom/resources/js/rollup.config.js b/components/ILIAS/Chatroom/resources/js/rollup.config.js new file mode 100644 index 000000000000..97411bbdd463 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/rollup.config.js @@ -0,0 +1,44 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import terser from '@rollup/plugin-terser'; +import copyright from '../../../../../scripts/Copyright-Checker/copyright'; +import preserveCopyright from '../../../../../scripts/Copyright-Checker/preserveCopyright'; + +export default [ + { + input: './src/index.js', + output: { + file: './dist/Chatroom.min.js', + format: 'iife', + banner: copyright, + plugins: [ + terser({ + format: { + comments: preserveCopyright, + }, + }), + ], + globals: { + il: 'il', + jquery: '$', + io: 'io', + }, + }, + external: ['il', 'jquery', 'io'], + }, + +]; diff --git a/components/ILIAS/Chatroom/resources/js/src/ChatMessageArea.js b/components/ILIAS/Chatroom/resources/js/src/ChatMessageArea.js new file mode 100644 index 000000000000..e10300d5e0ab --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/ChatMessageArea.js @@ -0,0 +1,257 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import il from 'il'; +import formatDateTime from './formatDateTime'; + +export default class ChatMessageArea { + /** @type {NodeElement} */ + #anchor; + /** @type {number} */ + #currentUserId; + /** @type {{scrolling: boolean, show_auto_msg: boolean}} */ + #config; + /** @type {ProfileImageLoader} */ + #profileImageLoader; + /** @type {WatchList} */ + #typingList; + /** @type {function(string): string} */ + #txt; + /** @type {{time: string, date: string}} */ + #format; + /** @type {null|{id: number, username: string, node: NodeElement}} */ + #lastUser; + /** @type {function(Date): bool} */ + #lastDate; + /** @type {NodeElement} */ + #pane; + /** @type {NodeElement} */ + #typingInfo; + /** @type {function(): void} */ + #touch; + + /** + * @param {NodeElement} anchor + * @param {number} currentUserId + * @param {{scrolling: boolean, show_auto_msg: boolean}} config + * @param {ProfileImageLoader} profileImageLoader + * @param {WatchList} typingList + * @param {function(string): string} txt + */ + constructor(anchor, currentUserId, config, profileImageLoader, typingList, txt, format) { + this.#anchor = anchor; + this.#currentUserId = currentUserId; + this.#config = config; + this.#profileImageLoader = profileImageLoader; + this.#typingList = typingList; + this.#txt = txt; + this.#format = format; + this.#pane = createDiv(['messageContainer']); + this.#pane.setAttribute('aria-live', 'polite'); + this.#typingInfo = createDiv(['typing-info']); + this.#typingInfo.setAttribute('aria-live', 'polite'); + this.#touch = Void; + + this.#syncConfig(); + this.clearMessages(); + this.#show(); + } + + addMessage(message) { + this.#touch(); + const line = createDiv(['messageLine', 'chat', !message.target || message.target.public ? 'public' : 'private']); + + const fallback = () => console.warn('Unknown message type: ', message.type); + let lastUser = null; + const setUser = x => {lastUser = x;}; + + const cases = { + message: () => { + const m = msg(timeInfo(message, this.#format), actualMessage(message)); + if (this.#lastDate(new Date(message.timestamp))) { + this.#pane.appendChild(separate(message, this.#format)); + setUser(null); + } + + if (message.from.id === this.#currentUserId) { + line.classList.add('myself'); + } + + if (this.#lastUser + && this.#lastUser.id === message.from.id + && this.#lastUser.username === message.from.username) { + this.#lastUser.node.appendChild(m); + setUser(this.#lastUser); + } else { + line.appendChild( + messageHeader(message, this.#profileImageLoader, this.#format) + ); + line.appendChild(m); + this.#pane.appendChild(line); + setUser({...message.from, node: line}); + } + }, + connected: fallback, + disconnected: fallback, + private_room_entered: fallback, + private_room_left: fallback, + notice: () => { + const node = createDiv(['separator', 'system-message']); + const content = createDiv([], 'p'); + content.innerHTML = this.#txt(message.content, message.data); + node.appendChild(content); + this.#pane.appendChild(node); + }, + error: fallback, + userjustkicked: fallback, + }; + + (cases[message.type] || fallback)(); + + this.#lastUser = lastUser; + if (this.#config.scrolling) { + this.#anchor.scrollTop = this.#pane.getBoundingClientRect().height; + } + } + + clearMessages() { + this.#pane.innerHTML = ''; + this.#lastUser = null; + this.#lastDate = remeberLastDate(); + + const node = createDiv(['separator']); + const content = createDiv([], 'p'); + content.textContent = this.#txt('welcome_to_chat'); + node.appendChild(content); + this.#pane.appendChild(node); + this.#touch = node.remove.bind(node); + } + + typingListChanged() { + const names = Object.values(this.#typingList.all()); + if (names.length === 0) { + this.#typingInfo.textContent = ''; + } else if (names.length === 1) { + this.#typingInfo.textContent = this.#txt("chat_user_x_is_typing", names[0]); + } else { + this.#typingInfo.textContent = this.#txt("chat_users_are_typing"); + } + } + + enableAutoScroll(enable) { + this.#config.scrolling = Boolean(enable); + this.#syncConfig(); + } + + enableSystemMessages(enable) { + this.#config.show_auto_msg = Boolean(enable); + this.#syncConfig(); + } + + #show() { + this.#anchor.appendChild(this.#pane); + const fader = createDiv(['fader']); + this.#anchor.appendChild(fader); + fader.appendChild(this.#typingInfo); + } + + #syncConfig() { + this.#anchor.classList[this.#config.show_auto_msg ? 'remove' : 'add']('hide-system-messages'); + } +} + +function remeberLastDate() { + let last = null; + return date => { + const showMessage = !last + || last.getDate() !== date.getDate() + || last.getMonth() !== date.getMonth() + || last.getFullYear() !== date.getFullYear(); + last = date; + return showMessage; + }; +} + +function separate(message, format) { + const node = createDiv(['separator']); + const content = createDiv([], 'p'); + content.textContent = formatDateTime(format.date, message.timestamp); + node.appendChild(content); + + return node; +} + +function messageHeader(message, profileImageLoader, format) { + const dateFlag = createDiv(['user'], 'span'); + const userFlag = createDiv(['user'], 'span'); + const img = createDiv([], 'img'); + const header = createDiv(['message-header']); + + dateFlag.textContent = formatDateTime(format.time, message.timestamp); + userFlag.textContent = message.from.username; + img.src = profileImageLoader.defaultImage(); + profileImageLoader.imageOfUser(message.from).then(Reflect.set.bind(null, img, 'src')); + + header.appendChild(img); + header.appendChild(userFlag); + header.appendChild(dateFlag); + + return header; +} + +const link = (() => { + let linkNode = node => { + try { + il.ExtLink.autolink(node); + } catch (e) { + console.error('Disabling url linking. Reason:', e); + linkNode = Void; + } + }; + + return n => linkNode(n); +})(); + +function actualMessage(message) { + const messageSpan = createDiv([], 'p'); + messageSpan.innerHTML = message.content; + link(messageSpan); + + return messageSpan; +} + +function msg(info, message) { + const node = createDiv(['message-body']); + node.appendChild(info); + node.appendChild(message); + + return node; +} + +function timeInfo(message, format) { + const info = createDiv(['time-info']); + info.textContent = formatDateTime(format.time, message.timestamp); + + return info; +} + +function createDiv(classes, nodeType) { + const div = document.createElement(nodeType || 'div'); + (classes || []).forEach(name => div.classList.add(name)); + return div; +} + +function Void(){} diff --git a/components/ILIAS/Chatroom/resources/js/src/ChatUsers.js b/components/ILIAS/Chatroom/resources/js/src/ChatUsers.js new file mode 100644 index 000000000000..becbc160133d --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/ChatUsers.js @@ -0,0 +1,182 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import bus from './bus'; + +const keygen = ((() => { + let nr = 0; + return () => { + nr++; + return 'key-' + nr; + }; +})()); + +const takeTicketAndOnClick = proc => takeTicketAndWait(node => node.addEventListener('click', proc)); + +/** + * This class renders all available action for a user in a chat room. + */ +export default class ChatUsers { + /** @type {NodeElement} */ + #anchor; + /** @type {function(string, object): Promise} */ + #send; + /** @type {function(number): boolean} */ + #isSelf; + /** @type {ProfileImageLoader} */ + #profileImageLoader; + /** @type {NodeElement} */ + #emptyMessage; + /** @type {obect.} */ + #users; + /** @type {string[]} */ + #visibleUsers; + /** @type {{name: string, label: string, callback: function(number)}[]} */ + #userActions; + + /** + * @param {NodeElement} anchor + * @param {function(string, object): Promise} send + * @param {function(number): boolean} isSelf + * @param {ProfileImageLoader} profileImageLoader + * @param {{name: string, label: string, callback: function(number)}[]} userActions + */ + constructor(anchor, send, isSelf, profileImageLoader, userActions) { + this.#anchor = anchor; + this.#send = send; + this.#isSelf = isSelf; + this.#profileImageLoader = profileImageLoader; + this.#emptyMessage = anchor && anchor.querySelector('.no_users'); + this.#users = {}; + this.#visibleUsers = []; + this.#userActions = userActions; + } + + userListChanged(diff) { + diff.removed.forEach(({key}) => this.remove(key)); + diff.added.forEach(({value}) => this.add(value)); + } + + add(user) { + if (this.#users[user.id]) { + return false; + } + + const node = this.#buildUserEntry(user); + if (!this.#isSelf(user.id)) { + this.#visibleUsers.push(String(user.id)); + } + this.#anchor.appendChild(node); + this.#users[user.id] = node; + this.#preventEmpty(); + + return true; + } + + remove(id) { + const node = this.#users[id]; + if (!node) { + return false; + } + + node.remove(); + this.#visibleUsers = this.#visibleUsers.filter(x => x !== id); + delete this.#users[id]; + this.#preventEmpty(); + + return true; + } + + setUsers(users) { + const ids = users.map(u => String(u.id)); + Object.keys(this.#users) + .filter(id => !ids.includes(id)) + .forEach(this.remove.bind(this)); + users.forEach(this.add.bind(this)); + } + + static actionList(txt, connector, confirmModal, startConversation) { + return [ + { + name: 'kick', + callback(userId) { + confirmModal('kick-modal').then(confirmed => { + if (confirmed) { + connector.kick(userId); + } + }); + }, + }, + { + name: 'ban', + callback(userId) { + confirmModal('ban-modal').then(confirmed => { + if (confirmed) { + connector.ban(userId); + } + }); + }, + }, + { + name: 'chat', + callback: startConversation, + } + ]; + } + + #buildUserEntry(user) { + const node = document.createElement('div'); + const itemLoaded = this.#send('view-userEntry', { + username: user.username, + user_id: user.id, + actions: Object.fromEntries(this.#userActions.map(({name, callback}) => [ + name, + takeTicketAndOnClick(() => callback(user.id)) + ])), + }).then(r => r.text()).then(html => setHTMLWithScripts(node, html)); + node.classList.add('ilChatroomUser'); + + Promise.all([itemLoaded, this.#profileImageLoader.imageOfUser(user)]).then(([_, img]) => { + Array.from(node.querySelectorAll('img'), n => n.setAttribute('src', img)); + }); + if (this.#isSelf(user.id)) { + node.classList.add('ilNoDisplay'); + } + + return node; + } + + #preventEmpty() { + this.#emptyMessage.classList[this.#visibleUsers.length ? 'add' : 'remove']('ilNoDisplay'); + } +} + +function setHTMLWithScripts(node, html) { + node.innerHTML = html; + Array.from(node.querySelectorAll('script'), script => { + const s = document.createElement('script'); + s.appendChild(document.createTextNode(script.innerHTML)); + script.parentNode.replaceChild(s, script); + }); + + return node; +} + +function takeTicketAndWait(proc) { + const key = keygen(); + bus.onArrived(key, proc); + return key; +} diff --git a/components/ILIAS/Chatroom/resources/js/src/ILIASConnector.js b/components/ILIAS/Chatroom/resources/js/src/ILIASConnector.js new file mode 100644 index 000000000000..24c8a225d3c3 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/ILIASConnector.js @@ -0,0 +1,118 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +/** + * This class connects the client to the related ILIAS environment. + * Communication is handled by sending JSON requests. + * The response of each request is handled through callbacks delivered by the ILIASResponseHandler + * which is passed through the constructor. + */ +export default class ILIASConnector { + /** @type {function(string, object): Promise} */ + #send; + /** @type {Logger} */ + #logger; + + /** + * @param {function(string, object): Promise} send + * @param {Logger} logger + */ + constructor(send, logger){ + this.#send = send; + this.#logger = logger; + } + + /** + * Sends a heartbeat to ILIAS in a delivered interval. + * It is used to keep the session for an ILIAS user open. + * + * @param {number} interval + */ + heartbeatInterval(interval) { + const ignore = () => {}; + window.setInterval(() => this.#sendRequest('poll', {}, ignore), interval); + } + + /** + * Sends a request to ILIAS to leave a private room. + */ + leavePrivateRoom() { + this.#logger.logILIASRequest('leavePrivateRoom'); + this.#sendRequest('privateRoom-leave'); + } + + /** + * Sends a request to ILIAS to invite a specific user to a private room. + * The invitation can be done by two types + * 1. byId + * 2. byLogin + * + * @param {string} userValue + * @param {string} invitationType + */ + inviteToPrivateRoom(userValue, invitationType) { + this.#sendRequest('inviteUsersToPrivateRoom-' + invitationType, { + user: userValue + }); + } + + /** + * Sends a request to ILIAS to clear the chat history + */ + clear() { + this.#sendRequest('clear'); + } + + /** + * Sends a request to ILIAS to kick a user from a specific room. + * The room can either be a private or the main room. + * + * @param {number} userId + */ + kick(userId) { + this.#sendRequest('kick', {user: userId}); + } + + /** + * Sends a request to ILIAS to ban a user from a specific room. + * The room can either be a private or the main room. + * + * @param {number} userId + */ + ban(userId) { + this.#sendRequest('ban-active', {user: userId}); + } + + /** + * Sends a asynchronously JSON request to ILIAS. + * + * @param {string} action + * @param {{}} params + * @param {function} responseCallback + */ + #sendRequest(action, params = {}, responseCallback = r => this.#gotResponse(r)) { + this.#send(action, params).then(r => r.json()).then(responseCallback); + } + + #gotResponse(response) { + this.#logger.logILIASResponse('default'); + if (!response.success) { + console.error(response.reason); + return false; + } + return true; + } +} diff --git a/components/ILIAS/Chatroom/resources/js/src/Logger.js b/components/ILIAS/Chatroom/resources/js/src/Logger.js new file mode 100644 index 000000000000..efb99a21bc47 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/Logger.js @@ -0,0 +1,37 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +export default class Logger { + logServerResponse(message) { + this.#log('Server-Response', message); + }; + + logServerRequest(message) { + this.#log('Server-Request', message); + } + + logILIASResponse(message) { + this.#log('ILIAS-Response', message); + } + + logILIASRequest(message) { + this.#log('ILIAS-Request', message); + } + + #log(type, message) { + console.log(type, message); + } +} diff --git a/components/ILIAS/Chatroom/resources/js/src/ProfileImageLoader.js b/components/ILIAS/Chatroom/resources/js/src/ProfileImageLoader.js new file mode 100644 index 000000000000..744dd65136f0 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/ProfileImageLoader.js @@ -0,0 +1,120 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +const DEBOUNCE_TIMOUT = 20; + +export default class ProfileImageLoader { + /** @type {string} */ + #url; + /** @type {string} */ + #fallbackImage; + /** @type {object.} */ + #loaded; + /** @type {object.}} */ + #queue; + /** @type {function(): void} */ + #reset; + + /** + * @param {string} url + * @param {string} fallbackImage + */ + constructor(url, fallbackImage) { + this.#url = url; + this.#fallbackImage = fallbackImage; + this.#loaded = {}; + this.#queue = {}; + this.#reset = () => {}; + } + + /** + * @param {object} user + * @returns {Promise.} + */ + imageOfUser(user) { + if (this.#loaded[this.#key(user)]) { + return Promise.resolve(this.#loaded[this.#key(user)]); + } + return this.#debounceLoad(user); + } + + /** + * @param {object[]} users + * @returns {Promise.} + */ + imagesOfUsers(users) { + return Promise.all(users.map(this.imageOfUser.bind(this))); + } + + defaultImage() { + return this.#fallbackImage; + } + + /** + * @param {object} user + */ + #debounceLoad(user) { + return new Promise((resolve, reject) => { + const key = this.#key(user); + this.#queue[key] = this.#queue[key] || {value: user, waiting: []}; + this.#queue[key].waiting.push({resolve, reject}); + this.#reset(); + this.#reset = clearTimeout.bind(null, setTimeout(this.#request.bind(this), DEBOUNCE_TIMOUT)); + }); + } + + #request() { + const profiles = Object.values(this.#queue).map(({value}) => value); + const loadImage = fetch(this.#url, { + method: 'POST', + body: JSON.stringify({profiles}), + headers: {'Content-Type': 'application/json'} + }).then(r => r.json()); + + loadImage.then(response => Object.entries(this.#flushQueue()).forEach( + ([key, {waiting}]) => waiting.forEach( + ({resolve, reject}) => { + if (response[key]) { + this.#loaded[key] = response[key]; + resolve(response[key]); + } else { + reject('Image not returned from server.'); + } + } + ) + )); + + loadImage.catch( + error => Object.values(this.#flushQueue()) + .flatMap(v => v.waiting) + .forEach(p => p.reject(error)) + ); + } + + /** + * @param {object} user + * @returns {string} + */ + #key(user) { + return JSON.stringify(user); + } + + #flushQueue() { + const queue = this.#queue; + this.#queue = {}; + return queue; + } +} diff --git a/components/ILIAS/Chatroom/resources/js/src/ServerConnector.js b/components/ILIAS/Chatroom/resources/js/src/ServerConnector.js new file mode 100644 index 000000000000..1056e76725d5 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/ServerConnector.js @@ -0,0 +1,291 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +/** + * This class connects the client to the related chat server. + * Communication is handled through websockets as far as *it is supported by the users browser. + * Otherwise it uses polling method to communicate. Messages are send through `socket.emit`. + * Messages are received through `socket.on`. There can be 3 types of messages. + * 1. Text messages which are send by the chat users + * 2. Notification messages. This are informational messages which are triggered by the system. + * 3. Action messages. This messages triggers action which have to be executed in the client. + * This messages are triggered by the System. + * + */ +export default class ServerConnector { + /** @type {{login: string, id: number, profile_picture_visible: boolean}} */ + #user; + /** @type {WatchList} */ + #userList; + /** @type {ChatMessageArea} */ + #chatArea; + /** @type {number} */ + #roomId; + /** @type {WatchList} */ + #typingList; + /** @type {string} */ + #redirectUrl; + /** @type {Logger} */ + #logger; + /** @type {Socket} */ + #socket; + + /** + * @param {{login: string, id: number, profile_picture_visible: boolean}} user + * @param {WatchList} userList + * @param {ChatMessageArea} chatArea + * @param {number} roomId + * @param {WatchList} typingList + * @param {string} redirectUrl + * @param {Logger} logger + */ + constructor(user, userList, chatArea, roomId, typingList, redirectUrl, logger) { + this.#user = user; + this.#userList = userList; + this.#chatArea = chatArea; + this.#roomId = roomId; + this.#typingList = typingList; + this.#redirectUrl = redirectUrl; + this.#logger = logger; + } + + init(socket) { + this.#socket = socket; + this.#socket.on('message', this.#onMessage.bind(this)); + this.#socket.on('connect', () => { + this.#socket.emit('login', this.#user.login, this.#user.id, this.#user.profile_picture_visible); + }); + this.#socket.on('user_invited', this.#onUserInvited.bind(this)); + this.#socket.on('private_room_entered', this.#onPrivateRoomEntered.bind(this)); + this.#socket.on('connected', this.#onConnected.bind(this)); + this.#socket.on('userjustkicked', this.#onUserKicked.bind(this)); + this.#socket.on('userjustbanned', this.#onUserBanned.bind(this)); + this.#socket.on('clear', this.#onClear.bind(this)); + this.#socket.on('notice', this.#onNotice.bind(this)); + this.#socket.on('userStartedTyping', this.#onUserStartedTyping.bind(this)); + this.#socket.on('userStoppedTyping', this.#onUserStoppedTyping.bind(this)); + this.#socket.on('userlist', this.#onUserlist.bind(this)); + this.#socket.on('shutdown', () => { + this.#socket.removeAllListeners(); + this.#socket.close(); + window.location.href = this.#redirectUrl; + }); + + window.addEventListener('beforeunload', () => { + this.#socket.close(); + }); + } + + /** + * Sends enter room to server + * + */ + enterRoom() { + // @Todo: Remove? caus' private room. + this.#logger.logServerRequest('enterRoom'); + this.#socket.emit('enterRoom', this.#roomId); + } + + /** + * @param {function} callback + */ + onLoggedIn(callback) { + this.#socket.on('loggedIn', callback); + } + + userStartedTyping() { + this.#logger.logServerRequest('userStartedTyping'); + this.#socket.emit('userStartedTyping', this.#roomId); + } + + userStoppedTyping() { + this.#logger.logServerRequest('userStoppedTyping'); + this.#socket.emit('userStoppedTyping', this.#roomId); + } + + sendMessage(message) { + this.#socket.emit('message', message, this.#roomId); + } + + /** + * Displays chatmessage in chat + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * from: {id: number, name: string}, + * format: {style: string, color: string, family: string, size: string} + * }} messageObject + * + */ + #onMessage(messageObject) { + this.#chatArea.addMessage(messageObject); + } + + /** + * Adds chat for user invitation + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * title: string + * owner: number + * }} messageObject + * + */ + #onUserInvited(messageObject) { + // gui.addChatMessageArea(messageObject.title, messageObject.owner); + } + + /** + * Enters a private Room + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * title: string, + * owner: number, + * subscriber: {id: number, username: string}, + * usersInRoom: {Array} + * }} messageObject + * + */ + #onPrivateRoomEntered(messageObject) { + this.#logger.logServerResponse('onPrivateRoomEntered'); + } + + #onConnected(messageObject) { + Object.values(messageObject.users).forEach(user => { + let data = { + id: user.id, + username: user.login, + profile_picture_visible: user.profile_picture_visible, + }; + + this.#userList.add(data); + this.#chatArea.addMessage({login: data.label, timestamp: messageObject.timestamp, type: 'connected'}); + }); + } + + /** + * Kicks a user from chat + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * }} messageObject + * + */ + #onUserKicked(messageObject) { + this.#logger.logServerResponse('onUserKicked'); + + // If user is kicked from sub room, redirect to main room + + this.#userList.remove(this.#user.id); + window.location.href = this.#redirectUrl + "&msg=kicked"; + } + + /** + * Banns a user from chat + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * }} messageObject + * + */ + #onUserBanned(messageObject) { + if (this.#socket) { + this.#socket.removeAllListeners(); + this.#socket.close(); + } + window.location.href = this.#redirectUrl + "&msg=banned"; + } + + /** + * Clears chat history + */ + #onClear() { + this.#chatArea.clearMessages(); + } + + /** + * Adds a notice to chat + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * data: {} + * }} messageObject + * + */ + #onNotice(messageObject) { + this.#chatArea.addMessage(messageObject); + } + + #onUserStartedTyping(message) { + this.#logger.logServerResponse("onUserStartedTyping"); + + const subscriber = JSON.parse(message.subscriber); + this.#typingList.add(subscriber.id, subscriber.username); + } + + #onUserStoppedTyping(message) { + this.#logger.logServerResponse("onUserStoppedTyping"); + + const subscriber = JSON.parse(message.subscriber); + this.#typingList.remove(subscriber.id); + } + + /** + * Updates the list of users. + * + * @param {{ + * type:string, + * timestamp: number, + * content: string, + * roomId: number, + * users: {} + * }} messageObject + * + */ + #onUserlist(messageObject) { + const users = messageObject.users; + this.#logger.logServerResponse("onUserlist"); + + this.#userList.setAll(Object.fromEntries(Object.values(users).map(otherUser => { + const chatUser = { + id: otherUser.id, + username: otherUser.username, + profile_picture_visible: otherUser.profile_picture_visible, + }; + + return [chatUser.id, chatUser]; + }))); + } +}; diff --git a/components/ILIAS/Chatroom/resources/js/src/Type.js b/components/ILIAS/Chatroom/resources/js/src/Type.js new file mode 100644 index 000000000000..b411f4d1d018 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/Type.js @@ -0,0 +1,74 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +const HEARTBEAT_TIMEOUT = 5000; + +/** + * Type like typing not the type. + * This interface is used to decide what happens when the user is typing or stops typing. + * + * @typedef {{release: function(): void, heartbeat: function(): void}} Type + */ + +/** + * This class implements {Type} and is used to notify the given {ServerConnector} whether or not the user is typing. + * @implements {Type} + */ +export class TypeSelf { + /** @type {ServerConnector} */ + #serverConnector; + /** @type {boolean} */ + #isTyping; + /** @type {function(): void} */ + #reset; + + /** + * @param {ServerConnector} serverConnector + */ + constructor(serverConnector) { + this.#serverConnector = serverConnector; + this.#isTyping = false; + this.#reset = () => {}; + window.addEventListener('beforeunload',this.release.bind(this)); + } + + release() { + this.#reset(); + if (this.#isTyping) { + this.#serverConnector.userStoppedTyping(); + this.#isTyping = false; + } + } + + heartbeat() { + this.#reset(); + if (!this.#isTyping) { + this.#serverConnector.userStartedTyping(); + this.#isTyping = true; + } + this.#reset = clearTimeout.bind(null, setTimeout(this.release.bind(this), HEARTBEAT_TIMEOUT)); + } +} + +/** + * This class implements {Type} and is used in case nothing should be notified when the user is typing. + * + * @implements {Type} + */ +export class TypeNothing { + release() {} + heartbeat() {} +} diff --git a/components/ILIAS/Chatroom/resources/js/src/WatchList.js b/components/ILIAS/Chatroom/resources/js/src/WatchList.js new file mode 100644 index 000000000000..6f4954c75b7f --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/WatchList.js @@ -0,0 +1,76 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +const diffLeft = (left, right) => Object.keys(left) + .filter(key => !Reflect.has(right, key)) + .map(key => ({key, value: left[key]})); + +export default class WatchList { + /** @type {object.} */ + #list; + /** @type {Array.} */ + #onChangeList; + + constructor() { + this.#list = {}; + this.#onChangeList = []; + } + + find(key) { + return this.#list[key]; + } + + has(key) { + return Reflect.has(this.#list, String(key)); + } + + onChange(callback) { + this.#onChangeList.push(callback); + } + + add(key, value) { + key = String(key); + this.#list[key] = value; + this.#changed({added: [{key, value}], removed: []}); + } + + remove(key) { + key = String(key); + if (!Reflect.has(this.#list, key)) { + return; + } + const value = this.#list[key]; + delete this.#list[key]; + this.#changed({added: [], removed: [{key, value}]}); + } + + setAll(list) { + const diff = { + added: diffLeft(list, this.#list), + removed: diffLeft(this.#list, list), + }; + this.#list = list; + this.#changed(diff); + } + + all() { + return this.#list; + } + + #changed(diff) { + this.#onChangeList.forEach(f => f(diff)); + } +} diff --git a/components/ILIAS/Chatroom/resources/js/src/bindSendMessageBox.js b/components/ILIAS/Chatroom/resources/js/src/bindSendMessageBox.js new file mode 100644 index 000000000000..5a509ad69b56 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/bindSendMessageBox.js @@ -0,0 +1,64 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import { expandableTextareaFromNodes } from './expandableTextarea'; + +export default (anchor, sendMessage, typing) => { + const textarea = anchor.querySelector('#submit_message_text'); + const button = anchor.querySelector('#submit_message'); + button.addEventListener('click', e => { + e.preventDefault(); + e.stopPropagation(); + send(); + }); + + const syncSize = expandableTextareaFromNodes(anchor.querySelector('#chat-shadow'), textarea, 3); + + textarea.addEventListener('input', syncSize); + + textarea.addEventListener('keydown', e => { + const keycode = e.keyCode || e.which; + + if (keycode === 13 && !e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); + + textarea.blur(); + send(); + } + }); + + textarea.addEventListener('keyup', e => { + typing[(e.keyCode || e.which) === 13 ? 'release' : 'heartbeat'](); + }); + + function send() { + const content = textarea.value; + + if (content.trim() !== '') { + const message = { + content, + format: {} + }; + + textarea.value = ''; + typing.release(); + sendMessage(message); + textarea.focus(); + syncSize(); + } + } +}; diff --git a/components/ILIAS/Chatroom/resources/js/src/bus.js b/components/ILIAS/Chatroom/resources/js/src/bus.js new file mode 100644 index 000000000000..7793aaa182d5 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/bus.js @@ -0,0 +1,42 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +export function createBus() { + const sent = {}; + const waiting = {}; + + return { + send(name, value) { + if (sent[name]) { + throw new Error('Name already provided.'); + } + sent[name] = value; + const callMe = (waiting[name] || []); + delete waiting[name]; + callMe.forEach(proc => proc(value)); + }, + onArrived(name, proc) { + if (sent[name]) { + proc(sent[name]); + return; + } + waiting[name] = waiting[name] || []; + waiting[name].push(proc); + }, + }; +} + +export default createBus(); diff --git a/components/ILIAS/Chatroom/resources/js/src/createConfirmation.js b/components/ILIAS/Chatroom/resources/js/src/createConfirmation.js new file mode 100644 index 000000000000..b6eed3be632f --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/createConfirmation.js @@ -0,0 +1,55 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import bus from './bus'; + +const Void = () => {}; + +const load = key => new Promise(resolve => bus.onArrived(key, ({node, showModal, closeModal}) => { + let next = Void; + node.querySelector('form').addEventListener('submit', e => { + e.preventDefault(); + next(true); + next = Void; + closeModal(); + return false; + }); + resolve(() => { + showModal(); + return new Promise(ok => { + next = ok; + }); + }); +})); + +const cached = proc => { + const cache = {}; + return key => { + if (!cache[key]) { + cache[key] = proc(key); + } + + return cache[key]; + }; +}; + +/** + * @returns {function(string): Promise.} + */ +export default () => { + const cachedLoad = cached(load); + return key => cachedLoad(key).then(f => f()); +}; diff --git a/components/ILIAS/Chatroom/resources/js/src/expandableTextarea.js b/components/ILIAS/Chatroom/resources/js/src/expandableTextarea.js new file mode 100644 index 000000000000..6017e2f9f517 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/expandableTextarea.js @@ -0,0 +1,133 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +const RELEVANT_STYLES = [ + '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', +]; + +const syncShadow = (textarea, shadow) => { + const style = window.getComputedStyle(textarea); + RELEVANT_STYLES.forEach(name => { + shadow.style[name] = style[name]; + }); +}; + +const willUpdate = textarea => { + /** Prevent style update if style is already set. */ + let currentHeight = ''; + return newHeight => { + if (newHeight !== currentHeight) { + textarea.style.height = newHeight; + currentHeight = newHeight; + } + }; +}; + +/** Return the height which would be added on newline. */ +const calculateLineHeight = shadow => { + const value = shadow.value; + shadow.value = ''; + const height = shadow.scrollHeight; + shadow.value = '\n'; + const lineHeight = shadow.scrollHeight - height; + shadow.value = value; + return lineHeight; +}; + +const calculateTextareaHeight = (shadow, maxLines) => { + const value = shadow.value; + shadow.value = '\n'.repeat(maxLines - 1); + const lineHeight = shadow.scrollHeight; + shadow.value = value; + return lineHeight; +}; + +const createShadow = textarea => { + const shadow = document.createElement('textarea'); + shadow.style.height = window.getComputedStyle(textarea).height; + shadow.setAttribute('area-hidden', 'true'); + shadow.readOnly = true; + shadow.disabled = true; + + return shadow; +}; + +const freeze = thunk => { + let thaw = () => { + const value = thunk(); + thaw = () => value; + return value; + }; + + return () => thaw(); +}; + +export function expandableTextarea(shadowBoxSelector, textareaSelector, maxLines) { + const select = selector => { + const node = document.querySelector(selector); + console.assert(node !== null, 'Could not find selector ' + JSON.stringify(selector)); + return node; + }; + return expandableTextareaFromNodes( + select(shadowBoxSelector), + select(textareaSelector), + maxLines + ); +} + +export function expandableTextareaFromNodes(shadowBox, textarea, maxLines) { + const shadow = createShadow(textarea); + const updateHeight = willUpdate(textarea); + const lineHeight = freeze(() => calculateLineHeight(shadow)); + + /** + * Max height of the textarea. + * !! This is not equal to maxLines * lineHeight() because it includes the base height. + */ + const maxTextareaHeight = freeze(() => calculateTextareaHeight(shadow, maxLines)); + + const lines = (initial, currentHeight) => parseInt( + ((currentHeight - initial) / lineHeight()) + 1 + ); + + const resize = () => { + shadow.value = ''; + const init = shadow.scrollHeight; + const height = textarea.clientHeight; + shadow.value = textarea.value; + const scroll = shadow.scrollHeight; + const currentLines = lines(init, scroll); + if (scroll > init) { + if (currentLines <= maxLines) { + updateHeight(scroll + 'px'); + } else { + updateHeight(maxTextareaHeight() + 'px'); + } + } else if (scroll < height) { + updateHeight(''); + } + }; + + return () => { + shadowBox.appendChild(shadow); + syncShadow(textarea, shadow); + resize(); + shadow.remove(); + }; +} diff --git a/components/ILIAS/Chatroom/resources/js/src/formatDateTime.js b/components/ILIAS/Chatroom/resources/js/src/formatDateTime.js new file mode 100644 index 000000000000..9d01cbc4f1b2 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/formatDateTime.js @@ -0,0 +1,33 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +const formatToXDigits = digits => nr => '0'.repeat(Math.max(0, digits - String(nr).length)) + nr; + +export default (format, dateOrTime) => { + const date = dateOrTime instanceof Date ? dateOrTime : new Date(dateOrTime); + const twoD = formatToXDigits(2); + + return [ + ['Y', date.getFullYear()], + ['m', twoD(date.getMonth() + 1)], + ['d', twoD(date.getDate())], + ['h', twoD((date.getHours() % 12) || 12)], + ['H', twoD(date.getHours())], + ['i', twoD(date.getMinutes())], + ['s', twoD(date.getSeconds())], + ['a', date.getHours() > 11 ? 'pm' : 'am'], + ].reduce((format, [from, to]) => format.replace(from, to), format); +}; diff --git a/components/ILIAS/Chatroom/resources/js/src/index.js b/components/ILIAS/Chatroom/resources/js/src/index.js new file mode 100644 index 000000000000..d949aa508ab0 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/index.js @@ -0,0 +1,27 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import il from 'il'; +import bus from './bus'; +import { expandableTextarea } from './expandableTextarea'; +import run, { runReadOnly } from './run'; + +il.Chatroom = { + run, + runReadOnly, + bus, + expandableTextarea, +}; diff --git a/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js b/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js new file mode 100644 index 000000000000..4891df4e9c84 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js @@ -0,0 +1,108 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +export default ({closeModal, showModal, node}, translation, iliasConnector, 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'); + closeModal(); + } + return false; + }); + + const reset = autocomplete(input, userList, send, v => { + value = v; + }); + + return () => { + reset(); + showModal(); + }; +}; + +function autocomplete(node, userList, send, setId) { + const p = node.parentNode; + const list = document.createElement('div'); + list.classList.add('chat-autocomplete'); + node.setAttribute('autocomplete', 'off'); + p.appendChild(list); + + const set = entry => { + setId(entry.id); + node.value = entry.value; + }; + + const search = debounce( + () => searchForUsers(userList, send, node.value).then(displayResults(list, set)), + 500 + ); + + node.addEventListener('input', () => { + setId(null); + search(); + }); + + return () => { + setId(null); + node.value = ''; + list.innerHTML = ''; + }; +} + +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 debounce(proc, delay) { + let del = () => {}; + return (...args) => { + return new Promise((ok, err) => { + del(); + del = window.clearTimeout.bind( + window, + window.setTimeout(() => proc(...args).then(ok).catch(err), delay) + ); + }); + }; +} + +const call = m => o => o[m](); + +function searchForUsers(userList, send, search) { + if (search.length < 3) { + return Promise.resolve([]); + } + return send('inviteUsersToPrivateRoom-getUserList', {q: search}).then(call('json')).then( + response => { + return response.items.filter(item => !userList.has(item.id)); + } + ); +} diff --git a/components/ILIAS/Chatroom/resources/js/src/run.js b/components/ILIAS/Chatroom/resources/js/src/run.js new file mode 100644 index 000000000000..2c8ec02b949f --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/run.js @@ -0,0 +1,191 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +import il from 'il'; +import io from 'io'; +import sendFromURL from './sendFromURL'; +import ILIASConnector from './ILIASConnector'; +import createConfirmation from './createConfirmation'; +import ProfileImageLoader from './ProfileImageLoader'; +import ChatUsers from './ChatUsers'; +import WatchList from './WatchList'; +import ChatMessageArea from './ChatMessageArea'; +import ServerConnector from './ServerConnector'; +import { TypeSelf, TypeNothing } from './Type'; +import bindSendMessageBox from './bindSendMessageBox'; +import bus from './bus'; +import inviteUserToRoom from './inviteUserToRoom'; +import Logger from './Logger'; +import willTranslate from './willTranslate'; + +const setup = options => { + const userList = new WatchList(); + const typingList = new WatchList(); + const logger = new Logger(); + const send = sendFromURL(options.apiEndpointTemplate); + const txt = willTranslate(options.lang, il.Language.txt.bind(il.Language)); + const confirmModal = createConfirmation(); + const profileLoader = new ProfileImageLoader( + options.initial.profile_image_url, + options.initial.no_profile_image_url + ); + const iliasConnector = new ILIASConnector(send, logger); + const chatUsers = new ChatUsers( + nodeById('chat_users'), + send, + id => id === options.initial.userinfo.id, + profileLoader, + filterAllowedActions( + ChatUsers.actionList(txt, iliasConnector, confirmModal, willStartConversation(userList)), + options.initial + ) + ); + const chatArea = new ChatMessageArea( + nodeById('chat_messages'), + options.initial.userinfo.id, + options.initial.state, + profileLoader, + typingList, + txt, + options.dateTimeFormatStrings, + ); + const serverConnector = new ServerConnector( + options.initial.userinfo, + userList, + chatArea, + options.scope, + typingList, + options.initial.redirect_url, + logger + ); + + return { + bindEvents, + processInitialData, + connectToServer, + }; + + function bindEvents() { + userList.onChange(chatUsers.userListChanged.bind(chatUsers)); + typingList.onChange(chatArea.typingListChanged.bind(chatArea)); + toggle('auto-scroll-toggle', on => chatArea.enableAutoScroll(on)); + toggle('system-messages-toggle', on => chatArea.enableSystemMessages(on)); + toggle('system-messages-toggle', on => saveShowSystemMessageState(on, options.initial)); + bus.onArrived('invite-modal', modalData => click('invite-button', inviteUserToRoom( + modalData, + txt, + iliasConnector, + userList, + send + ))); + + click('clear-history-button', () => clearHistory(confirmModal, iliasConnector)); + bindSendMessageBox( + nodeById('send-message-group'), + message => serverConnector.sendMessage(message), + options.initial.userinfo.broadcast_typing ? new TypeSelf(serverConnector) : new TypeNothing() + ); + } + + function processInitialData() { + popuplateInitialUserList(userList, options.initial); + populateInitialMessages(chatArea, options.initial); + } + + function connectToServer() { + iliasConnector.heartbeatInterval(120 * 1000); + serverConnector.init( + io.connect(options.baseUrl + '/' + options.instance, {path: options.initial.subdirectory}) + ); + serverConnector.onLoggedIn(() => { + serverConnector.enterRoom(options.scope, 0); + }); + } +}; + +export const runReadOnly = options => { + const {processInitialData} = setup(options); + processInitialData(); +}; + +export default options => { + const {bindEvents, processInitialData, connectToServer} = setup(options); + + bindEvents(); + processInitialData(); + connectToServer(); + nodeById('submit_message_text').focus(); +}; + +function filterAllowedActions(allActions, initial) { + const allowedActions = initial.userinfo.moderator ? ['kick', 'ban', 'chat'] : ['chat']; + return allActions.filter(option => allowedActions.includes(option.name)); +} + +function willStartConversation(userList) { + return userId => il.Chat.getConversation( + [il.OnScreenChat.user, userList.find(userId)] + ); +} + +function saveShowSystemMessageState(on, initial) { + return fetch(initial.system_message_update_url, { + method: 'POST', + body: new URLSearchParams({state: Number(on)}) + }); +} + +function clearHistory(confirmModal, iliasConnector) { + confirmModal('clear-history-modal').then(yes => { + if (yes) { + iliasConnector.clear(); + } + }); +} + +function popuplateInitialUserList(userList, initial) { + return userList.setAll(Object.fromEntries( + initial.users.map(user => { + const tmp = { + id: user.id, + username: user.login, + profile_picture_visible: user.profile_picture_visible, + }; + return [tmp.id, tmp]; + }) + )); +} + +function populateInitialMessages(chatArea, initial) { + return Object.values(initial.messages).forEach(message => { + message.timestamp = message.timestamp * 1000; + chatArea.addMessage(message); + }); +} + +function click(name, onClick) { + bus.onArrived(name, n => n.addEventListener('click', onClick)); +} + +function toggle(name, onChange) { + click(name, function() { + onChange(this.classList.contains('on')); + }); +} + +function nodeById(id) { + return document.getElementById(id); +} diff --git a/components/ILIAS/Chatroom/resources/js/src/sendFromURL.js b/components/ILIAS/Chatroom/resources/js/src/sendFromURL.js new file mode 100644 index 000000000000..adfc433cde13 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/sendFromURL.js @@ -0,0 +1,30 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +export default url => (action, getParameters = {}) => { + const target = new URL(url.replace(/postMessage/, action)); + Object.entries(getParameters).forEach(kv => set(target.searchParams, ...kv)); + + return fetch(target); +} + +function set(s, k, v) { + if (typeof v === 'object' && v !== null) { + Object.entries(v).forEach(([nk, nv]) => set(s, k + '[' + nk + ']', nv)); + } else { + s.set(k, v); + } +}; diff --git a/components/ILIAS/Chatroom/resources/js/src/willTranslate.js b/components/ILIAS/Chatroom/resources/js/src/willTranslate.js new file mode 100644 index 000000000000..612281af4c00 --- /dev/null +++ b/components/ILIAS/Chatroom/resources/js/src/willTranslate.js @@ -0,0 +1,28 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +export default (lang, fallback = k => '#'+k+'#') => (key, obj, ...rest) => { + let ret = lang[key]; + if (!ret) { + return fallback(key, obj, ...rest); + } + + Object.entries(obj || {}).forEach(([k, value]) => { + ret = ret.split('#' + k + '#').join(value); + }); + + return ret; +}; diff --git a/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html b/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html index 8a2e2cfbc6f1..5f3c0002013f 100755 --- a/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html +++ b/components/ILIAS/Chatroom/templates/default/tpl.chatroom.html @@ -1,55 +1,51 @@ - - -
    -

    - - - {LBL_USER_IN_ILIAS} -

    {LBL_NO_USER}
    -

    -
    - {CHAT_OUTPUT} {CHAT_INPUT} diff --git a/components/ILIAS/Chatroom/templates/default/tpl.chatroom_right.html b/components/ILIAS/Chatroom/templates/default/tpl.chatroom_right.html index 266511700b47..257ae86ae08c 100755 --- a/components/ILIAS/Chatroom/templates/default/tpl.chatroom_right.html +++ b/components/ILIAS/Chatroom/templates/default/tpl.chatroom_right.html @@ -1,4 +1,4 @@ -
    +

    {LBL_NO_FURTHER_USERS}

    @@ -8,21 +8,3 @@ [[LABEL]] - - diff --git a/components/ILIAS/Chatroom/templates/default/tpl.chatroom_send_message_form.html b/components/ILIAS/Chatroom/templates/default/tpl.chatroom_send_message_form.html index b17074679b6b..8ed37914ea66 100755 --- a/components/ILIAS/Chatroom/templates/default/tpl.chatroom_send_message_form.html +++ b/components/ILIAS/Chatroom/templates/default/tpl.chatroom_send_message_form.html @@ -1,14 +1,7 @@ -
    -
    -
    - -
    -
    - - -
    -
    -
    +
    +
    + + +
    + +
    diff --git a/components/ILIAS/Chatroom/templates/default/tpl.history.html b/components/ILIAS/Chatroom/templates/default/tpl.history.html index b64685b53daa..d1fa26da50f9 100755 --- a/components/ILIAS/Chatroom/templates/default/tpl.history.html +++ b/components/ILIAS/Chatroom/templates/default/tpl.history.html @@ -7,19 +7,5 @@

    {ROOM_TITLE}

    - - - - - -
    -   {MESSAGESENDER}:{MESSAGECONTENT} -
    - - - -

    {LBL_NO_MESSAGES}

    - + {CHAT}
    diff --git a/components/ILIAS/OnScreenChat/resources/onscreenchat.js b/components/ILIAS/OnScreenChat/resources/onscreenchat.js index e465ae777217..4338d40ff2ac 100644 --- a/components/ILIAS/OnScreenChat/resources/onscreenchat.js +++ b/components/ILIAS/OnScreenChat/resources/onscreenchat.js @@ -292,7 +292,7 @@ getModule().closeWindowWithLongestInactivity(); } - resizeTextareas[conversation.id] = expandableTextarea( + resizeTextareas[conversation.id] = il.Chatroom.expandableTextarea( '.panel-footer-for-shadow', '[data-onscreenchat-window="' + conversation.id + '"] [data-onscreenchat-message]', MAX_CHAT_LINES @@ -1611,111 +1611,4 @@ } return result; } - function freeze(thunk){ - let thaw = function(){ - const value = thunk(); - thaw = function(){return value;}; - return value; - }; - - return function(){ - return thaw(); - }; - } - - function expandableTextareaFromNodes(shadowBox, textarea, maxLines){ - const shadow = document.createElement('textarea'); - const updateHeight = (function(){ - /** Prevent style update if style is already set. */ - let currentHeight = ''; - return function(newHeight){ - if (newHeight !== currentHeight){ - textarea.style.height = newHeight; - currentHeight = newHeight; - } - }; - })(); - shadow.style.height = window.getComputedStyle(textarea).height; - shadow.setAttribute('area-hidden', 'true'); - shadow.readOnly = true; - shadow.disabled = true; - - const syncShadow = function(){ - const relevantStyles = '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'; - const style = window.getComputedStyle(textarea); - relevantStyles.split(' ').forEach(function(name){ - shadow.style[name] = style[name]; - }); - }; - - /** Return the height which would be added on newline. */ - const calculateLineHeight = function(){ - const value = shadow.value; - shadow.value = ''; - const height = shadow.scrollHeight; - shadow.value = '\n'; - const lineHeight = shadow.scrollHeight - height; - shadow.value = value; - return lineHeight; - }; - - const lineHeight = freeze(calculateLineHeight); - - /** - * Max height of the textarea. - * !! This is not equal to maxLines * lineHeight() because it includes the base height. - */ - const maxTextareaHeight = freeze(function(){ - const value = shadow.value; - shadow.value = '\n'.repeat(maxLines - 1); - const lineHeight = shadow.scrollHeight; - shadow.value = value; - return lineHeight; - }); - - const lines = function(initial, currentHeight){ - return parseInt(((currentHeight - initial) / lineHeight()) + 1); - }; - - const resize = function(){ - shadow.value = ''; - const init = shadow.scrollHeight; - const height = textarea.clientHeight; - shadow.value = textarea.value; - const scroll = shadow.scrollHeight; - const currentLines = lines(init, scroll); - if(scroll > init) - { - if(currentLines <= maxLines) - { - updateHeight(scroll + 'px'); - } - else - { - updateHeight(maxTextareaHeight() + 'px'); - } - } - else if(scroll < height) - { - updateHeight(''); - } - }; - - return function(){ - shadowBox.appendChild(shadow); - syncShadow(); - resize(); - shadow.remove(); - }; - } - - function expandableTextarea(shadowBoxSelector, textareaSelector, maxLines){ - const select = function(selector){ - const node = document.querySelector(selector); - console.assert(node !== null, 'Could not find selector ' + JSON.stringify(selector)); - return node; - }; - return expandableTextareaFromNodes(select(shadowBoxSelector), select(textareaSelector), maxLines); - } - })(jQuery, window, window.il.Chat, window.il.ChatDateTimeFormatter); diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 43cad7032c0f..e6498b25dd59 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -2890,8 +2890,8 @@ chatroom#:#chat_auth_token_info#:#Bitte definieren Sie hier zwei eindeutige Zeic chatroom#:#chat_ban#:#Sperren chatroom#:#chat_broadcast_typing#:#Tippen im Chat anzeigen chatroom#:#chat_broadcast_typing_info#:#Anderen Anwesenden in einem Chat bzw. Chatraum wird angezeigt, wenn Sie beginnen, eine Nachricht zu tippen. -chatroom#:#chat_connection_disconnected#:#--- #username# hat den Chatraum verlassen --- -chatroom#:#chat_connection_established#:#+++ #username# hat den Chatraum betreten +++ +chatroom#:#chat_connection_disconnected#:##username# hat den Chatraum verlassen. +chatroom#:#chat_connection_established#:##username# hat den Chatraum betreten. chatroom#:#chat_deletion_disabled#:#Deaktiviert chatroom#:#chat_deletion_interval#:#Intervall chatroom#:#chat_deletion_interval_info#:#Falls gewählt, werden Nachrichten im Magazin-Chat und im On-Screen-Chat gelöscht, sofern sie älter als der hier definierte Schwellenwert sind. @@ -2923,7 +2923,6 @@ chatroom#:#chat_mainroom#:#Hauptraum chatroom#:#chat_message#:#Nachricht chatroom#:#chat_message_display#:#Anzeigen chatroom#:#chat_message_options#:#Optionen -chatroom#:#chat_message_to_all#:#An alle chatroom#:#chat_no_use_typing_broadcast#:#Tippen wird anderen im Chat nicht angezeigt chatroom#:#chat_not_use_osc#:#Nutzt On-Screen-Chat nicht chatroom#:#chat_osc_accept_msg#:#Anchatten erlauben @@ -3017,7 +3016,7 @@ chatroom#:#kicked#:#Die Moderation hat Sie aus dem Chat entfernt.###Modified as chatroom#:#lost_connection#:#Die Verbindung zum Chat-Server wurde unterbrochen. chatroom#:#main#:#Hauptraum chatroom#:#messages#:#Nachrichten -chatroom#:#no_further_users#:#Keine weiteren Benutzer anwesend +chatroom#:#no_further_users#:#Keine weiteren Benutzer anwesend. chatroom#:#no_messages#:#Es sind keine Nachrichten für den ausgewählten Zeitraum verfügbar. chatroom#:#no_username_given#:#Bitte wählen Sie einen Anmeldenamen! chatroom#:#osc_browser_noti_no_permission_error#:#Bitte entfernen Sie die ILIAS-Domain von der Liste der blockierten Websites in den Benachrichtigungs-Einstellungen Ihres Browsers oder Betriebssystems. Andernfalls kann ILIAS keine Browser-Benachrichtigungen zustellen. @@ -3036,17 +3035,17 @@ chatroom#:#server_further_information#:#Weitere Informationen zur Konfiguration chatroom#:#session#:#Sitzung chatroom#:#settings_general#:#Allgemein chatroom#:#settings_title#:#Einstellungen des Chats -chatroom#:#speak_to#:##user# ansprechen +chatroom#:#start_private_chat#:#Privaten Chat starten chatroom#:#unable_to_connect#:#Die Verbindung zum Chat-Server konnte nicht hergestellt werden. chatroom#:#unban#:#Entsperren +chatroom#:#user_banned#:#Der Benutzer #user# wurde aus dem Chatraum gebannt. chatroom#:#user_in_ilias#:#Benutzer in ILIAS suchen und einladen chatroom#:#user_in_room#:#Benutzer im aktuellen Chatraum einladen chatroom#:#user_invited#:#Der Benutzer wurde eingeladen. -chatroom#:#user_invited_self#:#Sie wurden von #user# in den Chatraum #room# eingeladen. -chatroom#:#user_kicked#:#Der Benutzer #user# wurde aus dem Chatraum geworfen. +chatroom#:#user_invited_self#:#Sie wurden von #user# in den Chatraum #room# eingeladen. +chatroom#:#user_kicked#:#Der Benutzer #user# wurde aus dem Chatraum geworfen. chatroom#:#users#:#Benutzer -chatroom#:#welcome_to_chat#:#Willkommen im Chatraum -chatroom#:#whisper_to#:##user# anflüstern +chatroom#:#welcome_to_chat#:#Willkommen im Chatraum. chatroom#:#write_message#:#Neue Nachricht chatroom_adm#:#chat_cannot_connect_to_server#:#ILIAS kann keine Socket-Verbindung zum Chat-Server aufbauen. chatroom_adm#:#chat_enabled#:#Chat aktivieren diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index e52b2e836255..d852e96af52a 100755 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -2877,7 +2877,7 @@ chatroom#:#autogen_usernames#:#Auto Generated User Names chatroom#:#autogen_usernames_info#:#Pattern for auto generated user names that is assigned to anonymous user accounts. ‘#’ will be automatically replaced by a number. chatroom#:#ban_question#:#Do you really want to ban the user from chat? chatroom#:#ban_table_title#:#Banned Users -chatroom#:#banned#:#You have been banned from this chat room +chatroom#:#banned#:#You have been banned from this chat room. chatroom#:#bans#:#Bans chatroom#:#chat_address#:#Address chatroom#:#chat_anonymous_not_allowed#:#Please login to use the chat. @@ -2885,8 +2885,8 @@ chatroom#:#chat_auth_token_info#:#Please define unique strings used by ILIAS for chatroom#:#chat_ban#:#Ban chatroom#:#chat_broadcast_typing#:#Broadcast Typing chatroom#:#chat_broadcast_typing_info#:#If enabled, your typing will be broadcasted to other participants of a conversation or chat room. -chatroom#:#chat_connection_disconnected#:#--- #username# left the chat room --- -chatroom#:#chat_connection_established#:#+++ #username# has entered chat room +++ +chatroom#:#chat_connection_disconnected#:##username# left the chat room. +chatroom#:#chat_connection_established#:##username# has entered chat room. chatroom#:#chat_deletion_disabled#:#Disabled chatroom#:#chat_deletion_interval#:#Interval chatroom#:#chat_deletion_interval_info#:#If chosen, messages in the repository chat and On-Screen-Chat conversations will be deleted after the defined threshold. @@ -2918,7 +2918,6 @@ chatroom#:#chat_mainroom#:#Main Room chatroom#:#chat_message#:#Message chatroom#:#chat_message_display#:#Options chatroom#:#chat_message_options#:#Display -chatroom#:#chat_message_to_all#:#To All chatroom#:#chat_no_use_typing_broadcast#:#Typing will not be broadcasted chatroom#:#chat_not_use_osc#:#Not Using On-Screen-Chat chatroom#:#chat_osc_accept_msg#:#Allow On-Screen Chat Conversations @@ -3008,11 +3007,11 @@ chatroom#:#invite_to_private_room#:#Invite to Chat Room chatroom#:#invite_username#:#Username chatroom#:#key#:#Key chatroom#:#kick_question#:#Do you really want to kick the user from chat? -chatroom#:#kicked#:#You have been kicked from this chat room +chatroom#:#kicked#:#You have been kicked from this chat room. chatroom#:#lost_connection#:#The connection to the chat server was interrupted. chatroom#:#main#:#Main Room chatroom#:#messages#:#Messages -chatroom#:#no_further_users#:#No other users present +chatroom#:#no_further_users#:#No other users present. chatroom#:#no_messages#:#There are no saved messages for the given period. chatroom#:#no_username_given#:#Please choose a username chatroom#:#osc_browser_noti_no_permission_error#:#Please remove this domain from the list of blocked domains in the notification settings of your browser or operating system. Otherwise you will not be able to receive browser notifications. @@ -3031,17 +3030,17 @@ chatroom#:#server_further_information#:#You can find further information about t chatroom#:#session#:#Session chatroom#:#settings_general#:#General chatroom#:#settings_title#:#Settings -chatroom#:#speak_to#:#Speak to #user# +chatroom#:#start_private_chat#:#Start Private Chat chatroom#:#unable_to_connect#:#The connection to the chat server could not be established. chatroom#:#unban#:#Unban +chatroom#:#user_banned#:#The user #user# has been banned. chatroom#:#user_in_ilias#:#Search and invite user from ILIAS chatroom#:#user_in_room#:#Invite user from current chat room chatroom#:#user_invited#:#The user has been invited. -chatroom#:#user_invited_self#:##user# invited you to the chat room #room#. -chatroom#:#user_kicked#:#The user #user# has been kicked. +chatroom#:#user_invited_self#:##user# invited you to the chat room #room#. +chatroom#:#user_kicked#:#The user #user# has been kicked. chatroom#:#users#:#Users -chatroom#:#welcome_to_chat#:#Welcome to the chat room -chatroom#:#whisper_to#:#Whisper to #user# +chatroom#:#welcome_to_chat#:#Welcome to the chat room. chatroom#:#write_message#:#New Message chatroom_adm#:#chat_cannot_connect_to_server#:#ILIAS cannot build a socket connection to the chat server. chatroom_adm#:#chat_enabled#:#Enable Chat diff --git a/templates/default/070-components/legacy/Modules/_component_chatroom.scss b/templates/default/070-components/legacy/Modules/_component_chatroom.scss index 8868984bfbfb..c48209da2b92 100755 --- a/templates/default/070-components/legacy/Modules/_component_chatroom.scss +++ b/templates/default/070-components/legacy/Modules/_component_chatroom.scss @@ -1,7 +1,6 @@ @use "../../../010-settings/" as *; @use "../../../030-tools/_tool_browser-prefixes" as *; - /* Modules/Chatroom */ .ilValignBottom { @@ -32,7 +31,7 @@ .messageContainer { min-height: 250px; - } + } .fader { position: -webkit-sticky; @@ -71,9 +70,9 @@ } #chat_users { - overflow: auto; + overflow: visible; height: 100%; - min-height: 300px; + background-color: white; } #private_rooms { @@ -86,6 +85,39 @@ td.chatroom { height: auto; } +.chat-autocomplete { + display: flex; + position: relative; + width: 100%; + flex-direction: column; + max-height: 200px; + overflow: visible scroll; +} + +.chat-autocomplete button { + background-color: white; + border: 1px solid gray; + text-align: left; +} + +.chat-autocomplete button:hover { + background-color: lightgray; +} + +.chat-autocomplete button:focus { + background-color: lightgray; +} + +#chat_users .dropdown ul.dropdown-menu { + position: absolute; + left: unset; + right: 0; +} + +.ilChatroomUser img { + border-radius: 50%; +} + .ilChatroomUser { border-bottom: 1px solid #e9e9e9; @@ -97,6 +129,10 @@ td.chatroom { padding-top: 8px; } + .media-object { + border-radius: 50%; + } + .media-body h4, .media-body p { color: $il-text-light-color; font-size: $il-font-size-small; @@ -119,6 +155,14 @@ td.chatroom { font-size: $il-font-size-small; } + .ilChatroomDropdown { + position: relative; + + .dropdown-menu { + position: absolute; + } + } + .dropdown-menu a { color: $il-text-color; } @@ -152,6 +196,7 @@ td.chatroom { } .media { + overflow: visible; padding: 0; } @@ -173,3 +218,107 @@ td.chatroom { padding: 10px; } } + +.messageContainer .separator { + text-align: center; + background-color: #f9f9f9; +} + +.hide-system-messages .messageContainer .separator.system-message { + display: none; +} + +.messageContainer .separator p { + font-size: 0.75rem; + padding-top: 8px; + padding-bottom: 8px; +} + +.messageContainer img { + border-radius: 50%; + width: 30px; + height: 30px; +} + +.messageContainer .message-body { + display: flex; + align-items: center; +} + +.messageContainer .message-body > p { + margin: 0 5px; +} + +.messageContainer .message-body > .time-info { + color: white; + transition: color 0.5s; + font-size: smaller; +} + +.messageContainer .message-body > .time-info:hover { + color: black; +} + +.messageContainer .message-body > p { + white-space: preserve; +} + +.messageContainer .messageLine, +.messageContainer .messageLine .message-header { + display: flex; + margin-bottom: 5px; +} + +.messageContainer .messageLine { + flex-direction: column; +} + +.messageContainer .messageLine.myself .message-header, +.messageContainer .messageLine.myself .message-body { + flex-direction: row-reverse; + text-align: right; +} + +.messageContainer .messageLine .user { + margin: 0 5px; + font-size: 0.625rem; + font-weight: 600; +} + +.messageContainer .separator:not(:first-child) { + margin-top: 10px; +} + +#send-message-group .send-message-form { + width: 100%; + display: flex; + align-items: flex-start; +} + +#send-message-group .send-message-form textarea { + resize: none; + border: 1px solid; + background: white; + height: 25px; + margin-right: 10px; + flex-grow: 1; +} + +#chat-shadow { + height: 0; + width: 0; +} + +.chat.messageseparator { + padding: 0; + padding-right: 3px; +} + +#submit_message_text { + width: 80%; + display: inline; +} + +// #chat_users .il-item-title { +// display: inline-block; +// } diff --git a/templates/default/delos.css b/templates/default/delos.css index 9dbe6e685db9..d8cd1d7a4969 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -11894,9 +11894,9 @@ ul.il-book-obj-selection li { } } #chat_users { - overflow: auto; + overflow: visible; height: 100%; - min-height: 300px; + background-color: white; } #private_rooms { @@ -11909,6 +11909,39 @@ td.chatroom { height: auto; } +.chat-autocomplete { + display: flex; + position: relative; + width: 100%; + flex-direction: column; + max-height: 200px; + overflow: visible scroll; +} + +.chat-autocomplete button { + background-color: white; + border: 1px solid gray; + text-align: left; +} + +.chat-autocomplete button:hover { + background-color: lightgray; +} + +.chat-autocomplete button:focus { + background-color: lightgray; +} + +#chat_users .dropdown ul.dropdown-menu { + position: absolute; + left: unset; + right: 0; +} + +.ilChatroomUser img { + border-radius: 50%; +} + .ilChatroomUser { border-bottom: 1px solid #e9e9e9; } @@ -11918,6 +11951,9 @@ td.chatroom { .ilChatroomUser .media-body { padding-top: 8px; } +.ilChatroomUser .media-object { + border-radius: 50%; +} .ilChatroomUser .media-body h4, .ilChatroomUser .media-body p { color: #6f6f6f; font-size: 0.75rem; @@ -11937,6 +11973,12 @@ td.chatroom { padding: 10px 0; font-size: 0.75rem; } +.ilChatroomUser .ilChatroomDropdown { + position: relative; +} +.ilChatroomUser .ilChatroomDropdown .dropdown-menu { + position: absolute; +} .ilChatroomUser .dropdown-menu a { color: #161616; } @@ -11965,6 +12007,7 @@ td.chatroom { position: static; } .ilChatroomUser .media { + overflow: visible; padding: 0; } .ilChatroomUser .media-left img { @@ -11982,6 +12025,106 @@ td.chatroom { padding: 10px; } +.messageContainer .separator { + text-align: center; + background-color: #f9f9f9; +} + +.hide-system-messages .messageContainer .separator.system-message { + display: none; +} + +.messageContainer .separator p { + font-size: 0.75rem; + padding-top: 8px; + padding-bottom: 8px; +} + +.messageContainer img { + border-radius: 50%; + width: 30px; + height: 30px; +} + +.messageContainer .message-body { + display: flex; + align-items: center; +} + +.messageContainer .message-body > p { + margin: 0 5px; +} + +.messageContainer .message-body > .time-info { + color: white; + transition: color 0.5s; + font-size: smaller; +} + +.messageContainer .message-body > .time-info:hover { + color: black; +} + +.messageContainer .message-body > p { + white-space: preserve; +} + +.messageContainer .messageLine, +.messageContainer .messageLine .message-header { + display: flex; + margin-bottom: 5px; +} + +.messageContainer .messageLine { + flex-direction: column; +} + +.messageContainer .messageLine.myself .message-header, +.messageContainer .messageLine.myself .message-body { + flex-direction: row-reverse; + text-align: right; +} + +.messageContainer .messageLine .user { + margin: 0 5px; + font-size: 0.625rem; + font-weight: 600; +} + +.messageContainer .separator:not(:first-child) { + margin-top: 10px; +} + +#send-message-group .send-message-form { + width: 100%; + display: flex; + align-items: flex-start; +} + +#send-message-group .send-message-form textarea { + resize: none; + border: 1px solid; + background: white; + height: 25px; + margin-right: 10px; + flex-grow: 1; +} + +#chat-shadow { + height: 0; + width: 0; +} + +.chat.messageseparator { + padding: 0; + padding-right: 3px; +} + +#submit_message_text { + width: 80%; + display: inline; +} + /* Modules/Course */ .ilValignTop { vertical-align: top;