/** * 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 = '
' + '' + '' + '' + '' + '' + '' + '' + rows + '
' + escapeHtml(modalLabels.name) + '' + escapeHtml(modalLabels.login) + '' + escapeHtml(modalLabels.timein) + '' + escapeHtml(modalLabels.timeout) + '
'; } 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); });