Files
meetletacbfree/amd/build/view.min.js
T
mrxzamani 988aafce39 Commit AMD build artifacts required for Moodle Marketplace review.
Stop ignoring amd/build so generated modules ship with the plugin ZIP.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 12:42:19 +03:30

1053 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @module mod_meetletacbfree/view
* @copyright 2026 MeetLet <info@meetlet.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['core/ajax', 'core/notification', 'core/str'], function(Ajax, Notification, Str) {
function init() {
const SELECTORS = {
config: '[data-meetlet-config]',
app: '.meetlet-app',
tab: '.meetlet-tab',
panel: '[data-panel]',
join: '[data-action="join"]',
offline: '[data-action="makeoffline"]',
launchRecording: '[data-action="launch-recording"]',
launchContent: '[data-action="launch-content"]',
downloadContent: '[data-action="download-content"]',
refresh: '[data-action="refresh-tab"]',
overlay: '[data-join-overlay]',
sessionDetails: '[data-action="session-details"]',
modal: '[data-attendance-modal]',
modalBody: '[data-attendance-modal-body]',
modalTitle: '[data-attendance-modal-title]',
modalClose: '[data-attendance-modal-close]',
};
let state = {
cmid: 0,
groupid: 0,
current: 'details',
cache: {},
joinable: true,
status: 'open',
statusMessage: '',
pollSeconds: 15,
statusTimer: null,
recPollTimer: null,
licenseUsable: true,
};
const RECORDINGS_POLL_MS = 60000;
const DOWNLOAD_CONTENT_TIMEOUT_MS = 320000;
let modalLabels = {
name: '',
login: '',
timein: '',
timeout: '',
empty: '',
};
const detailsHtml = () => {
const tpl = document.querySelector('[data-meetlet-details-html]');
return tpl ? tpl.innerHTML : '';
};
const setLoading = (panel, text) => {
panel.innerHTML = '<div class="meetlet-loading"><span class="meetlet-loading__spinner"></span><span>' +
text + '</span></div>';
};
const showDetails = (panel) => {
panel.innerHTML = detailsHtml();
};
const escapeHtml = (value) => {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
let floatTipEl = null;
let floatTipTarget = null;
const ensureFloatTip = () => {
if (floatTipEl && document.body.contains(floatTipEl)) {
return floatTipEl;
}
floatTipEl = document.createElement('div');
floatTipEl.className = 'meetlet-floattip';
floatTipEl.setAttribute('role', 'tooltip');
floatTipEl.hidden = true;
document.body.appendChild(floatTipEl);
return floatTipEl;
};
const hideFloatTip = () => {
floatTipTarget = null;
const el = ensureFloatTip();
el.classList.remove('is-visible', 'is-below');
el.hidden = true;
el.textContent = '';
};
const placeFloatTip = (target) => {
const text = (target.getAttribute('data-tip') || '').trim();
if (!text) {
hideFloatTip();
return;
}
floatTipTarget = target;
const el = ensureFloatTip();
el.textContent = text;
el.hidden = false;
el.classList.remove('is-visible', 'is-below');
el.style.left = '0px';
el.style.top = '0px';
const tipW = el.offsetWidth;
const tipH = el.offsetHeight;
const r = target.getBoundingClientRect();
let left = r.left + (r.width / 2) - (tipW / 2);
left = Math.max(8, Math.min(left, window.innerWidth - tipW - 8));
let top = r.top - tipH - 10;
let below = false;
if (top < 8) {
top = r.bottom + 10;
below = true;
}
if (top + tipH > window.innerHeight - 8 && !below) {
top = Math.max(8, r.bottom + 10);
below = true;
}
el.style.left = left + 'px';
el.style.top = top + 'px';
el.classList.toggle('is-below', below);
requestAnimationFrame(() => {
if (floatTipTarget === target) {
el.classList.add('is-visible');
}
});
};
const bindFloatTips = (root) => {
if (!root || root.dataset.meetletTipsBound === '1') {
return;
}
root.dataset.meetletTipsBound = '1';
root.addEventListener('pointerover', (e) => {
const tip = e.target.closest('[data-tip]');
if (!tip || !root.contains(tip)) {
return;
}
placeFloatTip(tip);
});
root.addEventListener('pointerout', (e) => {
const tip = e.target.closest('[data-tip]');
if (!tip || !root.contains(tip)) {
return;
}
const next = e.relatedTarget;
if (next && tip.contains(next)) {
return;
}
if (floatTipTarget === tip) {
hideFloatTip();
}
});
root.addEventListener('focusin', (e) => {
const tip = e.target.closest('[data-tip]');
if (tip && root.contains(tip)) {
placeFloatTip(tip);
}
});
root.addEventListener('focusout', (e) => {
const tip = e.target.closest('[data-tip]');
if (!tip) {
return;
}
const next = e.relatedTarget;
if (next && tip.contains(next)) {
return;
}
if (floatTipTarget === tip) {
hideFloatTip();
}
});
};
const modalScroll = () => window.MeetletModalScroll || {
lock: () => {},
unlock: () => {},
focusNoScroll: (el) => {
if (el && typeof el.focus === 'function') {
el.focus();
}
},
};
const closeModal = () => {
const modal = document.querySelector(SELECTORS.modal);
if (!modal) {
return;
}
modal.setAttribute('hidden', 'hidden');
modal.style.display = 'none';
modalScroll().unlock();
};
const ensureModalShell = () => {
let modal = document.querySelector(SELECTORS.modal);
if (modal) {
return modal;
}
modal = document.createElement('div');
modal.className = 'meetlet-modal';
modal.setAttribute('data-attendance-modal', '1');
modal.setAttribute('hidden', 'hidden');
modal.style.display = 'none';
modal.innerHTML =
'<div class="meetlet-modal__backdrop" data-attendance-modal-close="1"></div>' +
'<div class="meetlet-modal__dialog" role="dialog" aria-modal="true">' +
'<div class="meetlet-modal__head">' +
'<h3 class="meetlet-modal__title" data-attendance-modal-title></h3>' +
'<button type="button" class="meetlet-modal__close" data-attendance-modal-close="1" aria-label="Close">×</button>' +
'</div>' +
'<div class="meetlet-modal__body" data-attendance-modal-body></div>' +
'</div>';
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target.closest(SELECTORS.modalClose)) {
closeModal();
}
});
return modal;
};
const openModal = (title, attendees) => {
const modal = ensureModalShell();
const titleEl = modal.querySelector(SELECTORS.modalTitle);
const body = modal.querySelector(SELECTORS.modalBody);
if (titleEl) {
titleEl.textContent = title || '';
}
if (!attendees || !attendees.length) {
body.innerHTML = '<div class="meetlet-empty">' + escapeHtml(modalLabels.empty) + '</div>';
} else {
let rows = '';
attendees.forEach((row) => {
rows += '<tr>' +
'<td>' + escapeHtml(row.name || '') + '</td>' +
'<td>' + escapeHtml(row.login || '') + '</td>' +
'<td>' + escapeHtml(row.timein || '-') + '</td>' +
'<td>' + escapeHtml(row.timeout || '-') + '</td>' +
'</tr>';
});
body.innerHTML =
'<div class="meetlet-tablewrap">' +
'<table class="meetlet-table meetlet-table--modal">' +
'<thead><tr>' +
'<th>' + escapeHtml(modalLabels.name) + '</th>' +
'<th>' + escapeHtml(modalLabels.login) + '</th>' +
'<th>' + escapeHtml(modalLabels.timein) + '</th>' +
'<th>' + escapeHtml(modalLabels.timeout) + '</th>' +
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
}
modalScroll().lock();
modal.removeAttribute('hidden');
modal.style.display = 'flex';
};
const readSessionsPayload = (panel) => {
const holder = panel.querySelector('[data-meetlet-sessions-json]');
if (!holder) {
return [];
}
try {
return JSON.parse(holder.textContent || '[]');
} catch (e) {
return [];
}
};
const setButtonBusy = (btn, busy) => {
if (!btn) {
return;
}
if (btn.tagName !== 'A') {
btn.disabled = !!busy;
}
btn.classList.toggle('is-busy', !!busy);
if (busy) {
btn.setAttribute('aria-busy', 'true');
if (!btn.querySelector('.meetlet-iconbtn__spinner')) {
const spin = document.createElement('span');
spin.className = 'meetlet-iconbtn__spinner';
spin.setAttribute('aria-hidden', 'true');
btn.appendChild(spin);
}
} else {
btn.removeAttribute('aria-busy');
const spin = btn.querySelector('.meetlet-iconbtn__spinner');
if (spin) {
spin.remove();
}
}
};
const parseDownloadFilename = (disposition, fallback) => {
if (!disposition) {
return fallback;
}
const utf8 = disposition.match(/filename\*=UTF-8''([^;\n]+)/i);
if (utf8 && utf8[1]) {
try {
return decodeURIComponent(utf8[1].trim());
} catch (e) {
return utf8[1].trim();
}
}
const quoted = disposition.match(/filename="([^"]+)"/i);
if (quoted && quoted[1]) {
return quoted[1].trim();
}
const plain = disposition.match(/filename=([^;\n]+)/i);
if (plain && plain[1]) {
return plain[1].trim().replace(/^"|"$/g, '');
}
return fallback;
};
const triggerFileDownload = (blob, filename) => {
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename || 'download';
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
};
const notifyDownloadError = (err) => {
const isAbort = err && err.name === 'AbortError';
const key = isAbort ? 'downloadcontenttimeout' : 'downloadcontentfailed';
return Str.get_string(key, 'meetletacbfree').then((message) => {
Notification.addNotification({message: message, type: 'error'});
return null;
}).catch(() => {
Notification.addNotification({
message: isAbort ? 'Download timed out.' : 'Download failed.',
type: 'error',
});
});
};
const downloadContentFile = (link) => {
const url = link.getAttribute('href');
const fallbackName = link.getAttribute('download') || 'download';
if (!url || link.classList.contains('is-busy')) {
return Promise.resolve();
}
setButtonBusy(link, true);
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), DOWNLOAD_CONTENT_TIMEOUT_MS);
return fetch(url, {
credentials: 'same-origin',
signal: controller.signal,
}).then((response) => {
if (!response.ok) {
throw new Error('http-' + response.status);
}
const filename = parseDownloadFilename(
response.headers.get('Content-Disposition'),
fallbackName
);
// Proxy delay is over once headers arrive — re-enable before blob finishes.
setButtonBusy(link, false);
return response.blob().then((blob) => {
if (!blob || blob.size <= 0) {
throw new Error('empty');
}
triggerFileDownload(blob, filename);
});
}).catch((err) => {
setButtonBusy(link, false);
return notifyDownloadError(err);
}).finally(() => {
window.clearTimeout(timer);
});
};
const endInlineRename = (wrap, restore) => {
const text = wrap.querySelector('.meetlet-recname__text');
const input = wrap.querySelector('.meetlet-recname__input');
const btn = wrap.querySelector('[data-action="inline-rename"]');
wrap.classList.remove('is-editing');
if (input) {
input.hidden = true;
if (restore && text) {
input.value = wrap.getAttribute('data-name') || text.textContent || '';
}
}
if (text) {
text.hidden = false;
}
if (btn) {
btn.hidden = false;
setButtonBusy(btn, false);
}
};
const saveInlineRename = (wrap) => {
const input = wrap.querySelector('.meetlet-recname__input');
const text = wrap.querySelector('.meetlet-recname__text');
const btn = wrap.querySelector('[data-action="inline-rename"]');
if (!input || wrap.dataset.saving === '1') {
return;
}
const current = wrap.getAttribute('data-name') || '';
const name = String(input.value || '').trim();
if (name === '' || name === current) {
endInlineRename(wrap, true);
return;
}
wrap.dataset.saving = '1';
if (btn) {
setButtonBusy(btn, true);
}
input.disabled = true;
Ajax.call([{
methodname: 'mod_meetletacbfree_rename_recording',
args: {
cmid: state.cmid,
recordingscoid: parseInt(wrap.getAttribute('data-recording'), 10),
name: name,
groupid: state.groupid,
},
}])[0].then((res) => {
wrap.setAttribute('data-name', name);
if (text) {
text.textContent = name;
}
input.value = name;
Notification.addNotification({message: res.message, type: 'success'});
endInlineRename(wrap, false);
const app = document.querySelector(SELECTORS.app);
const panel = app && app.querySelector(SELECTORS.panel);
if (state.current === 'recordings' && panel) {
state.cache.recordings = panel.innerHTML;
}
}).catch((err) => {
Notification.exception(err);
endInlineRename(wrap, true);
}).finally(() => {
wrap.dataset.saving = '0';
input.disabled = false;
});
};
const startInlineRename = (wrap) => {
const text = wrap.querySelector('.meetlet-recname__text');
const input = wrap.querySelector('.meetlet-recname__input');
const btn = wrap.querySelector('[data-action="inline-rename"]');
if (!input) {
return;
}
wrap.classList.add('is-editing');
if (text) {
text.hidden = true;
}
if (btn) {
btn.hidden = true;
}
input.hidden = false;
input.value = wrap.getAttribute('data-name') || (text ? text.textContent : '') || '';
input.focus();
input.select();
const onKey = (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
input.removeEventListener('keydown', onKey);
input.removeEventListener('blur', onBlur);
saveInlineRename(wrap);
} else if (ev.key === 'Escape') {
ev.preventDefault();
input.removeEventListener('keydown', onKey);
input.removeEventListener('blur', onBlur);
endInlineRename(wrap, true);
}
};
const onBlur = () => {
input.removeEventListener('keydown', onKey);
input.removeEventListener('blur', onBlur);
saveInlineRename(wrap);
};
input.addEventListener('keydown', onKey);
input.addEventListener('blur', onBlur);
};
const renderCached = (panel, tab) => {
panel.innerHTML = state.cache[tab];
bindPanelActions(panel);
};
const bindPanelActions = (panel) => {
panel.querySelectorAll(SELECTORS.offline).forEach((btn) => {
btn.addEventListener('click', () => {
setButtonBusy(btn, true);
Ajax.call([{
methodname: 'mod_meetletacbfree_make_offline',
args: {
cmid: state.cmid,
recordingscoid: parseInt(btn.dataset.recording, 10),
groupid: state.groupid,
},
}])[0].then((res) => {
setButtonBusy(btn, false);
Notification.addNotification({
message: res.message,
type: res.success ? 'success' : 'warning',
});
// Force re-fetch recordings after job request.
return loadTab('recordings', true);
}).catch((err) => {
setButtonBusy(btn, false);
const msg = (err && (err.message || err.error)) ? (err.message || err.error) : '';
if (msg) {
Notification.addNotification({message: msg, type: 'error'});
} else {
Notification.exception(err);
}
});
});
});
panel.querySelectorAll(SELECTORS.launchRecording).forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const scoid = parseInt(btn.dataset.recording, 10);
const mode = (btn.dataset.mode || 'play').toLowerCase();
if (!scoid) {
return;
}
setButtonBusy(btn, true);
setOverlay(true, 'recording');
Ajax.call([{
methodname: 'mod_meetletacbfree_get_recording_launch_url',
args: {
cmid: state.cmid,
recordingscoid: scoid,
mode: mode,
groupid: state.groupid,
},
}])[0].then((response) => {
setOverlay(false);
setButtonBusy(btn, false);
handleLaunchResponse(response);
return null;
}).catch((err) => {
setOverlay(false);
setButtonBusy(btn, false);
handleLaunchResponse(null, err);
});
});
});
panel.querySelectorAll(SELECTORS.launchContent).forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const scoid = parseInt(btn.dataset.content, 10);
if (!scoid) {
return;
}
setButtonBusy(btn, true);
setOverlay(true, 'content');
Ajax.call([{
methodname: 'mod_meetletacbfree_get_content_launch_url',
args: {
cmid: state.cmid,
contentscoid: scoid,
groupid: state.groupid,
},
}])[0].then((response) => {
setOverlay(false);
setButtonBusy(btn, false);
handleLaunchResponse(response);
return null;
}).catch((err) => {
setOverlay(false);
setButtonBusy(btn, false);
handleLaunchResponse(null, err);
});
});
});
panel.querySelectorAll(SELECTORS.downloadContent).forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
downloadContentFile(link);
});
});
panel.querySelectorAll('[data-action="inline-rename"]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const wrap = btn.closest('.meetlet-recname');
if (!wrap || wrap.classList.contains('is-editing')) {
return;
}
startInlineRename(wrap);
});
});
panel.onclick = (e) => {
const refreshBtn = e.target.closest(SELECTORS.refresh);
if (refreshBtn && panel.contains(refreshBtn)) {
e.preventDefault();
const tab = refreshBtn.getAttribute('data-tab') || state.current;
loadTab(tab, true);
return;
}
const btn = e.target.closest(SELECTORS.sessionDetails);
if (!btn || !panel.contains(btn)) {
return;
}
e.preventDefault();
const sessions = readSessionsPayload(panel);
const index = parseInt(btn.getAttribute('data-session-index') || '0', 10);
const session = sessions[index] || {title: '', attendees: []};
openModal(session.title || '', session.attendees || []);
};
};
const loadTab = (tab, force) => {
const app = document.querySelector(SELECTORS.app);
if (!app) {
return Promise.resolve();
}
const panel = app.querySelector(SELECTORS.panel);
state.current = tab;
app.querySelectorAll(SELECTORS.tab).forEach((btn) => {
const active = btn.dataset.tab === tab;
btn.classList.toggle('is-active', active);
btn.setAttribute('aria-selected', active ? 'true' : 'false');
});
if (tab === 'details') {
showDetails(panel);
return Promise.resolve();
}
if (!force && state.cache[tab]) {
renderCached(panel, tab);
if (tab === 'recordings') {
ensureRecordingsPoll();
}
return Promise.resolve();
}
return Str.get_string('loading', 'meetletacbfree').then((loading) => {
setLoading(panel, loading);
return Ajax.call([{
methodname: 'mod_meetletacbfree_get_tab',
args: {cmid: state.cmid, tab: tab, groupid: state.groupid},
}])[0];
}).then((response) => {
state.cache[tab] = response.html;
panel.innerHTML = response.html;
bindPanelActions(panel);
if (tab === 'recordings') {
resetRecordingsPoll();
}
return null;
}).catch(Notification.exception);
};
const pollRecordingsBackground = () => {
if (!state.cache.recordings) {
return;
}
Ajax.call([{
methodname: 'mod_meetletacbfree_get_tab',
args: {cmid: state.cmid, tab: 'recordings', groupid: state.groupid},
}])[0].then((response) => {
state.cache.recordings = response.html;
if (state.current !== 'recordings') {
return null;
}
const app = document.querySelector(SELECTORS.app);
const panel = app && app.querySelector(SELECTORS.panel);
if (panel) {
panel.innerHTML = response.html;
bindPanelActions(panel);
}
return null;
}).catch(() => null);
};
const stopRecordingsPoll = () => {
if (state.recPollTimer) {
window.clearInterval(state.recPollTimer);
state.recPollTimer = null;
}
};
const resetRecordingsPoll = () => {
stopRecordingsPoll();
state.recPollTimer = window.setInterval(pollRecordingsBackground, RECORDINGS_POLL_MS);
};
const ensureRecordingsPoll = () => {
if (!state.recPollTimer && state.cache.recordings) {
resetRecordingsPoll();
}
};
const setOverlay = (show, kind) => {
const overlay = document.querySelector(SELECTORS.overlay);
if (!overlay) {
return;
}
if (show) {
const titleEl = overlay.querySelector('.meetlet-joinoverlay__title');
const textEl = overlay.querySelector('.meetlet-joinoverlay__text');
const isRecording = kind === 'recording';
if (titleEl) {
titleEl.textContent = isRecording
? (state.strRecordingTitle || titleEl.textContent)
: (state.strJoiningTitle || titleEl.textContent);
}
if (textEl) {
textEl.textContent = isRecording
? (state.strRecordingText || textEl.textContent)
: (state.strJoiningText || textEl.textContent);
}
overlay.removeAttribute('hidden');
} else {
overlay.setAttribute('hidden', 'hidden');
}
};
const showGateModal = (title, message) => {
showHelpError({
message: message || '',
helpcode: '',
detailhtml: '',
}, title);
};
const bindCopyCode = (root) => {
if (!root) {
return;
}
root.querySelectorAll('[data-copy-code]').forEach((el) => {
el.style.cursor = 'pointer';
el.title = el.getAttribute('data-copy-hint') || el.textContent || '';
el.addEventListener('click', () => {
const code = el.getAttribute('data-copy-code') || el.textContent || '';
if (!code || !navigator.clipboard) {
return;
}
navigator.clipboard.writeText(code).then(() => {
el.classList.add('is-copied');
window.setTimeout(() => el.classList.remove('is-copied'), 1200);
}).catch(() => null);
});
});
};
const showHelpError = (payload, title) => {
const modal = ensureModalShell();
const titleEl = modal.querySelector(SELECTORS.modalTitle);
const body = modal.querySelector(SELECTORS.modalBody);
if (titleEl) {
titleEl.textContent = title || '';
}
const code = (payload && (payload.helpcode || payload.code)) || '';
const message = (payload && payload.message) || '';
const detail = (payload && payload.detailhtml) || '';
if (body) {
if (detail) {
body.innerHTML = detail;
} else {
let html = '<div class="meetlet-err meetlet-err--user">';
html += '<p class="meetlet-err__lead">' + escapeHtml(message) + '</p>';
if (code) {
html += '<div class="meetlet-err__codebox">';
html += '<span class="meetlet-err__codelabel"></span>';
html += '<code data-copy-code="' + escapeHtml(code) + '">' + escapeHtml(code) + '</code>';
html += '</div>';
}
html += '</div>';
body.innerHTML = html;
}
bindCopyCode(body);
}
modalScroll().lock();
modal.removeAttribute('hidden');
modal.style.display = 'flex';
};
const handleLaunchResponse = (response, fallbackErr) => {
if (response && response.ok && response.url) {
window.open(response.url, '_blank', 'noopener');
return;
}
if (response && (response.message || response.detailhtml || response.helpcode)) {
Str.get_string('joindeniedtitle', 'meetletacbfree').then((title) => {
showHelpError(response, title);
return null;
}).catch(() => showHelpError(response, ''));
return;
}
if (fallbackErr) {
const msg = (fallbackErr.message || fallbackErr.error) ? (fallbackErr.message || fallbackErr.error) : '';
if (msg) {
Str.get_string('joindeniedtitle', 'meetletacbfree').then((title) => {
showHelpError({message: msg}, title);
return null;
}).catch(() => Notification.exception(fallbackErr));
} else {
Notification.exception(fallbackErr);
}
}
};
const applyMeetingStatus = (info) => {
state.joinable = !!info.joinable;
state.status = info.status || state.status;
let message = info.message || '';
if (!state.joinable && (info.status === 'waitinghost' || info.errorcode === 'waitingforhost')) {
// Prefer the clearer moderator-wait explanation for the join tip.
message = state.strJoinWaiting || message;
}
state.statusMessage = message;
const wrap = document.querySelector('[data-meetlet-status-wrap]');
const badge = document.querySelector('[data-meetlet-status]');
const joinBtn = document.querySelector(SELECTORS.join);
const joinWrap = document.querySelector('[data-join-wrap]');
if (wrap) {
wrap.setAttribute('data-status', state.status);
}
if (badge) {
badge.className = 'meetlet-status meetlet-status--' + state.status;
badge.textContent = info.statuslabel || badge.textContent;
if (!state.joinable && state.statusMessage) {
badge.setAttribute('data-tip', state.statusMessage);
badge.setAttribute('tabindex', '0');
} else {
badge.removeAttribute('data-tip');
badge.removeAttribute('tabindex');
}
}
if (joinBtn) {
joinBtn.classList.toggle('is-disabled', !state.joinable);
if (state.joinable) {
joinBtn.removeAttribute('aria-disabled');
} else {
joinBtn.setAttribute('aria-disabled', 'true');
}
}
if (joinWrap) {
joinWrap.classList.toggle('is-blocked', !state.joinable);
joinWrap.removeAttribute('data-tip');
}
};
const CIRC = 2 * Math.PI * 16;
const setRingProgress = (remaining, total) => {
const ring = document.querySelector('[data-status-ring]');
const count = document.querySelector('[data-status-countdown]');
if (count) {
count.textContent = String(remaining);
}
if (ring) {
const ratio = total > 0 ? (remaining / total) : 0;
ring.style.strokeDasharray = String(CIRC);
ring.style.strokeDashoffset = String(CIRC * (1 - ratio));
}
};
const pollMeetingStatus = () => {
return Ajax.call([{
methodname: 'mod_meetletacbfree_get_meeting_status',
args: {cmid: state.cmid, groupid: state.groupid},
}])[0].then((info) => {
applyMeetingStatus(info);
return info;
}).catch(() => null);
};
const startStatusTicker = () => {
if (!state.licenseUsable || state.pollSeconds <= 0) {
return;
}
const total = state.pollSeconds || 15;
let remaining = total;
setRingProgress(remaining, total);
if (state.statusTimer) {
window.clearInterval(state.statusTimer);
}
const tick = () => {
remaining -= 1;
if (remaining <= 0) {
remaining = total;
setRingProgress(remaining, total);
pollMeetingStatus();
return;
}
setRingProgress(remaining, total);
};
state.statusTimer = window.setInterval(tick, 1000);
// After first paint / idle: ask Adobe for real room status, then every 15s via ticker.
const kickoff = () => {
pollMeetingStatus().then(() => {
remaining = total;
setRingProgress(remaining, total);
});
};
if (window.requestIdleCallback) {
window.requestIdleCallback(kickoff, {timeout: 1200});
} else {
window.setTimeout(kickoff, 0);
}
};
const joinMeeting = () => {
if (!state.joinable) {
Str.get_strings([
{key: 'joindeniedtitle', component: 'meetletacbfree'},
]).then((strings) => {
const msg = (state.status === 'waitinghost' || state.status === 'checking')
? (state.strJoinWaiting || state.statusMessage || '')
: (state.statusMessage || '');
showGateModal(strings[0], msg);
return null;
}).catch(() => {
showGateModal('', state.statusMessage || '');
});
return;
}
const joinBtn = document.querySelector(SELECTORS.join);
if (joinBtn) {
joinBtn.classList.add('is-busy');
}
setOverlay(true, 'meeting');
Ajax.call([{
methodname: 'mod_meetletacbfree_get_join_url',
args: {cmid: state.cmid, groupid: state.groupid},
}])[0].then((response) => {
setOverlay(false);
if (joinBtn) {
joinBtn.classList.remove('is-busy');
}
handleLaunchResponse(response);
return null;
}).catch((err) => {
setOverlay(false);
if (joinBtn) {
joinBtn.classList.remove('is-busy');
}
handleLaunchResponse(null, err);
pollMeetingStatus();
});
};
const cfgEl = document.querySelector(SELECTORS.config);
if (!cfgEl) {
return;
}
let config = {};
try {
config = JSON.parse(cfgEl.getAttribute('data-meetlet-config'));
} catch (e) {
return;
}
state.cmid = config.cmid;
state.groupid = config.groupid || 0;
state.pollSeconds = parseInt(config.statusPollSeconds || '15', 10) || 15;
state.strJoinWaiting = config.strJoinWaiting || '';
state.strJoiningTitle = config.strJoiningTitle || '';
state.strJoiningText = config.strJoiningText || '';
state.strRecordingTitle = config.strRecordingTitle || '';
state.strRecordingText = config.strRecordingText || '';
state.licenseUsable = config.licenseUsable !== false;
if (typeof config.joinable === 'boolean') {
state.joinable = config.joinable;
}
if (config.status) {
state.status = config.status;
}
if (config.statusMessage) {
state.statusMessage = config.statusMessage;
}
modalLabels = {
name: config.strAttendeeName || '',
login: config.strAttendeeLogin || '',
timein: config.strTimeIn || '',
timeout: config.strTimeOut || '',
empty: config.strNoAttendance || '',
};
const app = document.querySelector(SELECTORS.app);
if (!app) {
return;
}
bindFloatTips(app);
window.addEventListener('scroll', () => {
if (floatTipTarget) {
hideFloatTip();
}
}, true);
window.addEventListener('resize', hideFloatTip);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hideFloatTip();
closeModal();
}
});
window.addEventListener('pagehide', stopRecordingsPoll);
app.querySelectorAll(SELECTORS.tab).forEach((btn) => {
btn.addEventListener('click', () => {
loadTab(btn.dataset.tab, false);
});
});
const joinBtn = app.querySelector(SELECTORS.join);
if (joinBtn) {
joinBtn.addEventListener('click', joinMeeting);
}
startStatusTicker();
loadTab('details', false);
}
return {
init: init
};
});