/**
* MeetLet activity view — page-local only (not AMD).
* Loaded only on mod/meetletacbmarketplace/view.php via $PAGE->requires->js().
*/
require(['core/ajax', 'core/notification', 'core/str'], function(Ajax, Notification, Str) {
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 = '
' +
text + '
';
};
const showDetails = (panel) => {
panel.innerHTML = detailsHtml();
};
const escapeHtml = (value) => {
return String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
};
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 =
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
';
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 = '' + escapeHtml(modalLabels.empty) + '
';
} else {
let rows = '';
attendees.forEach((row) => {
rows += '' +
'| ' + escapeHtml(row.name || '') + ' | ' +
'' + escapeHtml(row.login || '') + ' | ' +
'' + escapeHtml(row.timein || '-') + ' | ' +
'' + escapeHtml(row.timeout || '-') + ' | ' +
'
';
});
body.innerHTML =
'' +
'
' +
'' +
'| ' + escapeHtml(modalLabels.name) + ' | ' +
'' + escapeHtml(modalLabels.login) + ' | ' +
'' + escapeHtml(modalLabels.timein) + ' | ' +
'' + escapeHtml(modalLabels.timeout) + ' | ' +
'
' + rows + '
';
}
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, 'meetletacbmarketplace').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_meetletacbmarketplace_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_meetletacbmarketplace_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_meetletacbmarketplace_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_meetletacbmarketplace_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', 'meetletacbmarketplace').then((loading) => {
setLoading(panel, loading);
return Ajax.call([{
methodname: 'mod_meetletacbmarketplace_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_meetletacbmarketplace_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 = '';
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', 'meetletacbmarketplace').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', 'meetletacbmarketplace').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_meetletacbmarketplace_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: 'meetletacbmarketplace'},
]).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_meetletacbmarketplace_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);
});