Skip to content

Commit

Permalink
OnScreenChat: Replace legacy modals with ui modals
Browse files Browse the repository at this point in the history
  • Loading branch information
lscharmer authored and mjansenDatabay committed Oct 10, 2024
1 parent 591add0 commit e29d5d2
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 199 deletions.
7 changes: 6 additions & 1 deletion components/ILIAS/Chatroom/classes/BuildChat.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
use ilTemplate;
use ilObjUser;
use ilCalendarSettings;
use ILIAS\UI\Factory as UIFactory;
use ILIAS\UI\Renderer as UIRenderer;

class BuildChat
{
Expand All @@ -38,7 +40,9 @@ public function __construct(
private readonly ilChatroomObjectGUI $gui,
private readonly ilChatroom $room,
private readonly ilChatroomServerSettings $settings,
private readonly ilObjUser $user
private readonly ilObjUser $user,
private readonly UIFactory $ui_factory,
private readonly UIRenderer $ui_renderer,
) {
}

Expand All @@ -56,6 +60,7 @@ public function template(bool $read_only, array $initial, string $input, string
$set_json_var('INITIAL_USERS', $this->room->getConnectedUsers());
$set_json_var('DATE_FORMAT', (string) $this->user->getDateFormat());
$set_json_var('TIME_FORMAT', $this->timeFormat());
$set_json_var('NOTHING_FOUND', $this->ui_renderer->render($this->ui_factory->messageBox()->info($this->ilLng->txt('chat_osc_no_usr_found'))));

$room_tpl->setVariable('CHAT_OUTPUT', $output);
$room_tpl->setVariable('CHAT_INPUT', $input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,6 @@ private function buildUserActions(int $user_id, array $actions): array

private function buildChat(ilChatroom $room, ilChatroomServerSettings $settings): BuildChat
{
return new BuildChat($this->ilCtrl, $this->ilLng, $this->gui, $room, $settings, $this->ilUser);
return new BuildChat($this->ilCtrl, $this->ilLng, $this->gui, $room, $settings, $this->ilUser, $this->uiFactory, $this->uiRenderer);
}
}

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions components/ILIAS/Chatroom/resources/js/src/WatchList.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default class WatchList {
return Reflect.has(this.#list, String(key));
}

includes(key) {
return this.has(key);
}

onChange(callback) {
this.#onChangeList.push(callback);
}
Expand Down
7 changes: 6 additions & 1 deletion components/ILIAS/Chatroom/resources/js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
*********************************************************************/

import il from 'il';
import bus from './bus';
import bus, { createBus } from './bus';
import { expandableTextarea } from './expandableTextarea';
import inviteUserToRoom from './inviteUserToRoom';
import sendFromURL from './sendFromURL';
import run, { runReadOnly } from './run';

il.Chatroom = {
run,
runReadOnly,
createBus,
bus,
expandableTextarea,
inviteUserToRoom,
sendFromURL,
};
165 changes: 125 additions & 40 deletions components/ILIAS/Chatroom/resources/js/src/inviteUserToRoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@
*
*********************************************************************/

export default ({closeModal, showModal, node}, translation, iliasConnector, userList, send) => {
export default ({closeModal, showModal, node}, labels, invite, userList, send) => {
const input = node.querySelector('input[type=text]');
let value = null;

node.querySelector('form').addEventListener('submit', e => {
e.preventDefault();
if (value != null) {
iliasConnector.inviteToPrivateRoom(value, 'byId');
invite(value);
closeModal();
}
return false;
});

const reset = autocomplete(input, userList, send, v => {
const reset = autocomplete(input, labels, userList, send, v => {
value = v;
});

Expand All @@ -37,52 +37,136 @@ export default ({closeModal, showModal, node}, translation, iliasConnector, user
};
};

function autocomplete(node, userList, send, setId) {
const p = node.parentNode;
const list = document.createElement('div');
function autocomplete(input, labels, userList, send, setValue) {
const parent = input.parentNode;
const container = document.createElement('div');
const list = document.createElement('ul');
const nothingFound = document.createElement('div');
container.classList.add('chat-autocomplete-container');
container.setAttribute('aria-live', 'asertive');
container.setAttribute('aria-relevant', 'additions');
container.setAttribute('role', 'status');
list.classList.add('chat-autocomplete');
node.setAttribute('autocomplete', 'off');
p.appendChild(list);
list.classList.add('ilNoDisplay');
nothingFound.classList.add('ilNoDisplay');
nothingFound.appendChild(labels.nothingFound);
input.setAttribute('autocomplete', 'off');
container.appendChild(list);
container.appendChild(nothingFound);
parent.appendChild(container);

const set = entry => {
setId(entry.id);
node.value = entry.value;
const select = entry => {
setValue(entry);
input.value = entry.value;
};

const search = debounce(
() => searchForUsers(userList, send, node.value).then(displayResults(list, set)),
500
);
const search = sendSearch => searchForUsers(userList, sendSearch, input.value).then(displayResults(input, list, nothingFound, select, {
load: () => search(s => send({...s, all: true})),
label: labels.more,
}));

const searchDelayed = debounce(() => search(send), 500);

node.addEventListener('input', () => {
setId(null);
search();
input.addEventListener('input', () => {
setValue(null);
searchDelayed();
});

input.addEventListener('keydown', e => {
if (e.key === 'ArrowDown' && list.firstChild) {
list.firstChild.querySelector('button').focus();
} else if (e.key === 'ArrowUp' && list.lastChild) {
list.lastChild.querySelector('button').focus();
}
})

return () => {
setId(null);
node.value = '';
setValue(null);
input.value = '';
list.innerHTML = '';
list.classList.add('ilNoDisplay');
nothingFound.classList.add('ilNoDisplay');
};
}

function displayResults(node, set) {
return results => {
node.innerHTML = '';
results.forEach(entry => {
const b = document.createElement('button');
b.textContent = entry.label;
node.appendChild(b);
b.addEventListener('click', () => {
node.innerHTML = '';
set(entry);
});
});
function displayResults(input, list, nothingFound, select, more) {
return ({items: results, hasMoreResults, inputTooShort}) => {
const clearList = () => {
list.innerHTML = '';
list.classList.add('ilNoDisplay');
};
const willSelect = entry => () => {
clearList();
select(entry);
};
if (results.length === 0) {
if (inputTooShort) {
clearList();
} else {
clearList();
nothingFound.classList.remove('ilNoDisplay');
return;
}
} else {
list.innerHTML = '';
list.classList.remove('ilNoDisplay');
}

nothingFound.classList.add('ilNoDisplay');
results.forEach(entry => list.appendChild(createResultItem(entry, input, willSelect(entry))));

if (hasMoreResults) {
list.appendChild(createLoadMoreItem(more.label, () => {
clearList();
more.load();
}));
}
};
}

function createResultItem(entry, input, select)
{
const li = document.createElement('li');
const button = document.createElement('button');

button.setAttribute('tabindex', '0');
button.textContent = entry.label;
button.addEventListener('click', select);
button.addEventListener('keydown', e => {
const cases = {
'Enter': select,
'ArrowDown': () => {
(li.nextSibling || {querySelector: () => input}).querySelector('button').focus();
},
'ArrowUp': () => {
(li.previousSibling || {querySelector: () => input}).querySelector('button').focus();
},
};

(cases[e.key] || Void)();
});

li.appendChild(button);

return li;
}

function createLoadMoreItem(label, loadMore)
{
const li = document.createElement('li');
const button = document.createElement('button');
button.classList.add('load-more');
button.textContent = label;

li.appendChild(button);

button.addEventListener('click', loadMore);

return li;
}

function debounce(proc, delay) {
let del = () => {};
let del = Void;
return (...args) => {
return new Promise((ok, err) => {
del();
Expand All @@ -94,15 +178,16 @@ function debounce(proc, delay) {
};
}

const call = m => o => o[m]();

function searchForUsers(userList, send, search) {
if (search.length < 3) {
return Promise.resolve([]);
return Promise.resolve({items: [], hasMoreResults: false, inputTooShort: true});
}
return send('inviteUsersToPrivateRoom-getUserList', {q: search}).then(call('json')).then(
response => {
return response.items.filter(item => !userList.has(item.id));
}
return send({search}).then(
response => ({
items: response.items.filter(item => !userList.includes(item.id)),
hasMoreResults: response.hasMoreResults || false,
})
);
}

function Void() {}
9 changes: 6 additions & 3 deletions components/ILIAS/Chatroom/resources/js/src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,13 @@ const setup = options => {
toggle('system-messages-toggle', on => saveShowSystemMessageState(on, options.initial));
bus.onArrived('invite-modal', modalData => click('invite-button', inviteUserToRoom(
modalData,
txt,
iliasConnector,
{
more: '»' + txt('autocomplete_more'),
nothingFound: options.nothingFound,
},
value => iliasConnector.inviteToPrivateRoom(value.id, 'byId'),
userList,
send
({search, all}) => send('inviteUsersToPrivateRoom-getUserList', Object.assign({q: search}, all ? {fetchall: '1'} : {})).then(r => r.json())
)));

click('clear-history-button', () => clearHistory(confirmModal, iliasConnector));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
initial: {INITIAL_DATA},
apiEndpointTemplate: {POSTURL},
dateTimeFormatStrings: {date: {DATE_FORMAT}, time: {TIME_FORMAT}},
nothingFound: new DOMParser().parseFromString({NOTHING_FOUND}, 'text/html').body.firstChild,
lang,
};
{JS_CALL}(options);
Expand Down
2 changes: 1 addition & 1 deletion components/ILIAS/Chatroom/tests/ilChatroomUserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public function testGetUsernameFromIlObjUser(): void
],
]);

$this->ilUserMock->expects($this->once())->method('getLogin')->willReturn($username);
$this->ilUserMock->expects($this->once())->method('getPublicName')->willReturn($username);
$this->ilChatroomMock->method('getRoomId')->willReturn($roomId);

$this->assertSame($username, $this->user->getUsername());
Expand Down
45 changes: 38 additions & 7 deletions components/ILIAS/OnScreenChat/classes/class.ilOnScreenChatGUI.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ public function executeCommand(): void
);
break;

case 'inviteModal':
$this->dic->language()->loadLanguageModule('chatroom');
$txt = $this->dic->language()->txt(...);
$modal = $this->dic->ui()->factory()->modal()->roundtrip($txt('chat_osc_invite_to_conversation'), $this->dic->ui()->factory()->legacy($txt('chat_osc_search_modal_info')), [
$this->dic->ui()->factory()->input()->field()->text($txt('chat_osc_user')),
])->withSubmitLabel($txt('confirm'));
$response = $this->renderAsyncModal('inviteModal', $modal);
break;

case 'confirmRemove':
$this->dic->language()->loadLanguageModule('chatroom');
$txt = $this->dic->language()->txt(...);
$modal = $this->dic->ui()->factory()->modal()->interruptive(
$txt('chat_osc_leave_grp_conv'),
$txt('chat_osc_sure_to_leave_grp_conv'),
''
)->withActionButtonLabel($txt('confirm'));
$response = $this->renderAsyncModal('confirmRemove', $modal);
break;

case 'getUserlist':
default:
$response = $this->getUserList();
Expand Down Expand Up @@ -198,15 +218,16 @@ public static function initializeFrontend(ilGlobalTemplateInterface $page): void
false,
'components/ILIAS/OnScreenChat'
))->get(),
'modalTemplate' => (new ilTemplate(
'tpl.chat-add-user.html',
false,
false,
'components/ILIAS/OnScreenChat'
))->get(),
'nothingFoundTemplate' => $DIC->ui()->renderer()->render($DIC->ui()->factory()->messageBox()->info($DIC->language()->txt('chat_osc_no_usr_found'))),
'userId' => $DIC->user()->getId(),
'username' => $DIC->user()->getLogin(),
'userListURL' => $DIC->ctrl()->getLinkTargetByClass(
'modalURLTemplate' => ILIAS_HTTP_PATH . '/' . $DIC->ctrl()->getLinkTargetByClass(
ilOnScreenChatGUI::class,
'postMessage',
null,
true
),
'userListURL' => ILIAS_HTTP_PATH . '/' . $DIC->ctrl()->getLinkTargetByClass(
'ilonscreenchatgui',
'getUserList',
'',
Expand Down Expand Up @@ -288,6 +309,9 @@ public static function initializeFrontend(ilGlobalTemplateInterface $page): void
iljQueryUtil::initjQueryUI($page);
ilLinkifyUtil::initLinkify($page);

$page->addJavaScript('assets/js/modal.js');
$page->addJavaScript('assets/js/socket.io.min.js');
$page->addJavaScript('assets/js/Chatroom.min.js');
$page->addJavaScript('assets/js/jquery.ui.touch-punch.js');
$page->addJavascript('assets/js/LegacyModal.js');
$page->addJavascript('assets/js/moment-with-locales.min.js');
Expand All @@ -311,4 +335,11 @@ public static function initializeFrontend(ilGlobalTemplateInterface $page): void
self::$frontend_initialized = true;
}
}

private function renderAsyncModal(string $bus_name, $modal)
{
return $this->getResponseWithText($this->dic->ui()->renderer()->renderAsync($modal->withAdditionalOnLoadCode(fn ($id) => (
'il.OnScreenChat.bus.send(' . json_encode($bus_name, JSON_THROW_ON_ERROR) . ', ' . json_encode([(string) $modal->getShowSignal(), (string) $modal->getCloseSignal()], JSON_THROW_ON_ERROR) . ');'
))));
}
}
Loading

0 comments on commit e29d5d2

Please sign in to comment.