';
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
};
});