(function () {
const els = {};
const state = {
role: 'user',
token: null,
user: null,
sinceId: 0,
polling: null,
targetUserId: 0, // admin selected user
live: 0,
};
function q(id) { return document.getElementById(id); }
function initEls() {
els.app = q('ptwcApp');
els.logo = q('ptwcLogo');
els.livePill = q('ptwcLivePill');
els.statusLine = q('ptwcStatusLine');
els.logoutBtn = q('ptwcLogoutBtn');
els.loginPanel = q('ptwcLoginPanel');
els.chatPanel = q('ptwcChatPanel');
els.tabs = els.app.querySelectorAll('.ptwc-tab');
els.username = q('ptwcUsername');
els.password = q('ptwcPassword');
els.loginBtn = q('ptwcLoginBtn');
els.loginError = q('ptwcLoginError');
els.adminBar = q('ptwcAdminBar');
els.userSelect = q('ptwcUserSelect');
els.kickBtn = q('ptwcKickBtn');
els.unkickBtn = q('ptwcUnkickBtn');
els.liveToggle = q('ptwcLiveToggle');
els.messages = q('ptwcMessages');
els.input = q('ptwcInput');
els.sendBtn = q('ptwcSendBtn');
els.transcriptEmail = q('ptwcTranscriptEmail');
els.sendTranscriptBtn = q('ptwcSendTranscriptBtn');
els.transcriptStatus = q('ptwcTranscriptStatus');
}
function api(path, opts = {}) {
const headers = Object.assign({
'Content-Type': 'application/json',
'X-WP-Nonce': PTWC.nonce,
}, opts.headers || {});
if (state.token) headers['X-PTWC-Token'] = state.token;
return fetch(PTWC.restUrl + path, Object.assign({}, opts, { headers }))
.then(async (r) => {
const data = await r.json().catch(() => ({}));
if (!r.ok) throw data;
return data;
});
}
function setStatus(text) {
els.statusLine.textContent = text;
}
function showError(msg) {
els.loginError.style.display = 'block';
els.loginError.textContent = msg || 'Login failed.';
}
function clearError() {
els.loginError.style.display = 'none';
els.loginError.textContent = '';
}
function setRole(role) {
state.role = role;
els.tabs.forEach(t => t.classList.toggle('active', t.dataset.role === role));
}
function saveSession() {
localStorage.setItem('ptwc_session', JSON.stringify({
token: state.token,
user: state.user,
role: state.role,
targetUserId: state.targetUserId,
sinceId: state.sinceId
}));
}
function loadSession() {
try {
const raw = localStorage.getItem('ptwc_session');
if (!raw) return false;
const obj = JSON.parse(raw);
if (!obj || !obj.token) return false;
state.token = obj.token;
state.user = obj.user || null;
state.role = obj.role || 'user';
state.targetUserId = obj.targetUserId || 0;
state.sinceId = obj.sinceId || 0;
return true;
} catch { return false; }
}
function clearSession() {
localStorage.removeItem('ptwc_session');
state.token = null;
state.user = null;
state.sinceId = 0;
state.targetUserId = 0;
}
function renderLogoVisibility() {
const show = (state.role === 'admin' && state.live === 1);
els.logo.style.display = show ? 'flex' : 'none';
}
function fmtLocalTime(gmtString) {
// gmtString like "2025-12-25 20:15:00" (GMT)
// Convert to ISO-ish: "2025-12-25T20:15:00Z"
const iso = gmtString.replace(' ', 'T') + 'Z';
const d = new Date(iso);
if (isNaN(d.getTime())) return gmtString + ' GMT';
return d.toLocaleString();
}
function addMessage(m) {
const div = document.createElement('div');
div.className = 'ptwc-msg ' + (m.sender_role === 'admin' ? 'admin' : 'user');
const meta = document.createElement('div');
meta.className = 'ptwc-meta';
const sender = document.createElement('div');
sender.className = 'ptwc-sender';
sender.textContent = m.sender_role === 'admin' ? `Admin (${m.sender_name})` : m.sender_name;
const time = document.createElement('div');
time.className = 'ptwc-time';
time.textContent = fmtLocalTime(m.created_at);
meta.appendChild(sender);
meta.appendChild(time);
const content = document.createElement('div');
content.className = 'ptwc-content';
content.innerHTML = m.content_html; // already escaped server-side + linkified
div.appendChild(meta);
div.appendChild(content);
els.messages.appendChild(div);
}
function scrollToBottom(smooth = true) {
const el = els.messages;
if (!el) return;
el.scrollTo({
top: el.scrollHeight,
behavior: smooth ? 'smooth' : 'auto'
});
}
function enterChatUI() {
els.loginPanel.style.display = 'none';
els.chatPanel.style.display = 'flex';
els.logoutBtn.style.display = 'inline-block';
setRole(state.role);
setStatus(state.role === 'admin' ? 'Admin connected.' : 'Connected.');
clearError();
els.adminBar.style.display = (state.role === 'admin') ? 'block' : 'none';
}
function exitChatUI() {
els.loginPanel.style.display = 'flex';
els.chatPanel.style.display = 'none';
els.logoutBtn.style.display = 'none';
els.messages.innerHTML = '';
els.transcriptStatus.textContent = '';
setStatus('Please log in.');
clearError();
renderLogoVisibility();
}
async function login() {
clearError();
const username = (els.username.value || '').trim();
const password = (els.password.value || '').trim();
if (!username || !password) {
showError('Enter username and password.');
return;
}
const res = await api('/login', {
method: 'POST',
body: JSON.stringify({ username, password, role: state.role })
});
if (!res.ok) {
showError(res.error || 'Login failed.');
return;
}
state.token = res.token;
state.user = res.user;
state.live = res.live || 0;
state.sinceId = 0;
// admin must choose a target user to view
if (state.role === 'admin') {
setStatus('Admin connected. Select a user conversation.');
}
saveSession();
enterChatUI();
renderLogoVisibility();
await pollOnce(true);
startPolling();
}
async function sendMessage() {
const text = (els.input.value || '').trim();
if (!text) return;
els.input.value = '';
const payload = { content: text };
if (state.role === 'admin') {
if (!state.targetUserId) {
setStatus('Select a user conversation first.');
return;
}
payload.target_user_id = state.targetUserId;
}
try {
await api('/send', {
method: 'POST',
body: JSON.stringify(payload)
});
await pollOnce(true);
} catch (e) {
if (e && e.kicked) {
handleKicked();
} else {
setStatus('Send failed. Please try again.');
}
}
}
function handleKicked() {
stopPolling();
setStatus('You were dismissed from the chat.');
clearSession();
exitChatUI();
showError('You were dismissed from the chat.');
}
function setUsersDropdown(users) {
if (state.role !== 'admin') return;
const current = state.targetUserId;
els.userSelect.innerHTML = '';
const placeholder = document.createElement('option');
placeholder.value = '0';
placeholder.textContent = '— Select a user —';
els.userSelect.appendChild(placeholder);
users.forEach(u => {
const opt = document.createElement('option');
opt.value = String(u.id);
opt.textContent = u.username + (u.is_kicked ? ' (dismissed)' : '');
els.userSelect.appendChild(opt);
});
els.userSelect.value = String(current || 0);
}
async function pollOnce(autoscroll) {
const params = new URLSearchParams();
params.set('since_id', String(state.sinceId || 0));
if (state.role === 'admin' && state.targetUserId) {
params.set('target_user_id', String(state.targetUserId));
}
const res = await api('/poll?' + params.toString(), { method: 'GET' });
state.live = res.live || 0;
renderLogoVisibility();
if (state.role === 'admin') {
setUsersDropdown(res.users || []);
els.liveToggle.checked = state.live === 1;
}
if (res.kicked) {
handleKicked();
return;
}
const msgs = res.messages || [];
if (msgs.length) {
msgs.forEach(m => {
addMessage(m);
state.sinceId = Math.max(state.sinceId, m.id);
});
saveSession();
if (autoscroll) scrollToBottom(true);
}
}
function startPolling() {
stopPolling();
state.polling = setInterval(() => pollOnce(false).catch(() => {}), 1500);
}
function stopPolling() {
if (state.polling) clearInterval(state.polling);
state.polling = null;
}
async function logout() {
try {
await api('/logout', { method: 'POST', body: JSON.stringify({}) });
} catch {}
stopPolling();
clearSession();
exitChatUI();
}
async function sendTranscript() {
const email = (els.transcriptEmail.value || '').trim();
if (!email) {
els.transcriptStatus.textContent = 'Enter an email address.';
return;
}
const payload = { email };
if (state.role === 'admin' && state.targetUserId) payload.target_user_id = state.targetUserId;
els.transcriptStatus.textContent = 'Sending transcript...';
try {
const res = await api('/transcript', {
method: 'POST',
body: JSON.stringify(payload)
});
els.transcriptStatus.textContent = res.ok ? 'Transcript sent.' : 'Could not send transcript.';
} catch {
els.transcriptStatus.textContent = 'Could not send transcript.';
}
}
async function adminKick(kick) {
if (!state.targetUserId) {
setStatus('Select a user first.');
return;
}
await api('/admin/kick', {
method: 'POST',
body: JSON.stringify({ user_id: state.targetUserId, kick: kick ? 1 : 0 })
});
await pollOnce(false);
}
async function adminSetLive(live) {
await api('/admin/live', {
method: 'POST',
body: JSON.stringify({ live: live ? 1 : 0 })
});
await pollOnce(false);
}
function bindEvents() {
els.tabs.forEach(t => t.addEventListener('click', () => setRole(t.dataset.role)));
els.loginBtn.addEventListener('click', () => login().catch(e => {
showError((e && e.error) ? e.error : 'Login failed.');
}));
els.password.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') els.loginBtn.click();
});
els.sendBtn.addEventListener('click', () => sendMessage().catch(() => {}));
els.input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter' && !ev.shiftKey) {
ev.preventDefault();
els.sendBtn.click();
}
});
els.logoutBtn.addEventListener('click', () => logout());
els.sendTranscriptBtn.addEventListener('click', () => sendTranscript());
els.userSelect.addEventListener('change', async () => {
state.targetUserId = parseInt(els.userSelect.value || '0', 10) || 0;
state.sinceId = 0;
els.messages.innerHTML = '';
saveSession();
await pollOnce(true);
scrollToBottom(false);
});
els.kickBtn.addEventListener('click', () => adminKick(true).catch(() => {}));
els.unkickBtn.addEventListener('click', () => adminKick(false).catch(() => {}));
els.liveToggle.addEventListener('change', () => {
adminSetLive(els.liveToggle.checked).catch(() => {});
});
}
async function bootstrap() {
initEls();
bindEvents();
// set initial live status
try {
const s = await api('/status', { method: 'GET' });
state.live = s.live || 0;
renderLogoVisibility();
} catch {}
// Try restore session
if (loadSession()) {
setRole(state.role);
enterChatUI();
renderLogoVisibility();
await pollOnce(true).catch(() => {
// session invalid
clearSession();
exitChatUI();
});
startPolling();
scrollToBottom(false);
} else {
exitChatUI();
}
}
document.addEventListener('DOMContentLoaded', bootstrap);
})();