';
+ html += '
' + escapeHtml(message) + '
';
+ if (code) {
+ html += '
';
+ html += '';
+ html += '' + escapeHtml(code) + '';
+ html += '
';
+ }
+ html += '
';
+ 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
+ };
+});