'use strict'; document.documentElement.classList.add('js-ready'); /* ========================================================= AUDIO FX ========================================================= */ var AudioFX = (function () { var ctx = null; function getCtx() { if (ctx) return ctx; try { var Ctx = window.AudioContext || window.webkitAudioContext; if (!Ctx) return null; ctx = new Ctx(); } catch (e) { ctx = null; } return ctx; } function resume() { var c = getCtx(); if (c && c.state === 'suspended') { try { c.resume(); } catch (e) {} } } function tone(freq, dur, type, gain, delay, freqEnd) { var c = getCtx(); if (!c) return; var t0 = c.currentTime + (delay || 0); var osc = c.createOscillator(); var g = c.createGain(); osc.type = type || 'sine'; osc.frequency.setValueAtTime(freq, t0); if (freqEnd !== undefined) { try { osc.frequency.exponentialRampToValueAtTime(freqEnd, t0 + dur); } catch (e) {} } g.gain.setValueAtTime(0.0001, t0); g.gain.exponentialRampToValueAtTime(gain || 0.12, t0 + 0.008); g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); osc.connect(g); g.connect(c.destination); osc.start(t0); osc.stop(t0 + dur + 0.04); } return { unlock: resume, like: function () { resume(); tone(1046.5, 0.22, 'sine', 0.11, 0); tone(1568.0, 0.14, 'sine', 0.05, 0); tone(2093.0, 0.06, 'sine', 0.025, 0); }, unlike: function () { resume(); tone(659.3, 0.08, 'sine', 0.05, 0); }, send: function () { resume(); tone(1046.5, 0.10, 'sine', 0.08, 0, 1568.0); tone(1568.0, 0.05, 'sine', 0.03, 0.06); }, receive: function () { resume(); tone(1568.0, 0.10, 'sine', 0.08, 0); tone(1046.5, 0.18, 'sine', 0.09, 0.08); }, delete: function () { resume(); tone(392.0, 0.12, 'sine', 0.06, 0, 196.0); tone(196.0, 0.08, 'sine', 0.03, 0.10); }, notify: function () { resume(); tone(392.0, 0.14, 'sine', 0.09, 0); tone(523.3, 0.14, 'sine', 0.09, 0.13); tone(659.3, 0.22, 'sine', 0.10, 0.26); } }; })(); /* ========================================================= TITLE FLASH ========================================================= */ var TitleFlash = (function () { var original = null; var active = false; var handler = null; function restore() { if (original !== null) document.title = original; active = false; if (handler) { document.removeEventListener('visibilitychange', handler); handler = null; } } return { flash: function (text) { if (!document.hidden) return; if (active) return; original = document.title; active = true; document.title = text; handler = function () { if (!document.hidden) restore(); }; document.addEventListener('visibilitychange', handler); } }; })(); (function globalUnlock() { function unlock() { try { AudioFX.unlock(); } catch (e) {} ['click', 'touchstart', 'keydown', 'touchmove', 'scroll'].forEach(function (evt) { try { window.removeEventListener(evt, unlock); } catch (e) {} }); } ['click', 'touchstart', 'keydown', 'touchmove', 'scroll'].forEach(function (evt) { window.addEventListener(evt, unlock, { once: true, passive: true }); }); })(); /* ========================================================= CORE + REAL-TIME + ISLAND + AVATAR MODAL + DELETE SOUND ========================================================= */ (function () { function resolveUrl(path) { var brandEl = document.querySelector('a.brand'); var base = ''; if (brandEl) { var href = brandEl.getAttribute('href') || '/'; base = href.replace(/\/+$/, ''); } var clean = String(path || '').replace(/^\/+/, ''); if (base === '') return '/' + clean; return base + '/' + clean; } function getLoginUrl() { var link = document.querySelector('a[href$="/login"], a[href="/login"]'); if (link) return link.getAttribute('href'); return resolveUrl('login'); } function getCsrfValue() { var meta = document.querySelector('meta[name="csrf-token"]'); return meta ? (meta.getAttribute('content') || '') : ''; } function fetchJson(url, options) { return fetch(url, options).then(function (r) { return r.text().then(function (text) { var json = {}; try { json = JSON.parse(text); } catch (e) { if (text && text.length < 400) { json = { ok: false, error: 'Server: ' + text.replace(/\s+/g, ' ').slice(0, 200) }; } else { json = { ok: false, error: 'Server error (HTTP ' + r.status + ')' }; } } if (!r.ok && !json.error) json.error = 'HTTP ' + r.status; if (typeof json.status !== 'number') json.status = r.status; return json; }); }); } function showStoryToast(message) { var toast = document.getElementById('story-toast'); if (!toast) { try { console.log('[toast]', message); } catch (e) {} return; } toast.textContent = message; toast.classList.add('is-visible'); clearTimeout(showStoryToast._t); showStoryToast._t = setTimeout(function () { toast.classList.remove('is-visible'); }, 2200); } /* ---- AVATAR MODAL ---- */ function initAvatarModal() { var modal = document.getElementById('avatar-modal'); var modalImg = document.getElementById('avatar-modal-img'); if (!modal || !modalImg) return; document.addEventListener('click', function (event) { var trigger = event.target && event.target.closest ? event.target.closest('[data-avatar-full]') : null; if (!trigger) return; event.preventDefault(); event.stopPropagation(); var src = trigger.getAttribute('data-avatar-full'); if (!src) return; modalImg.src = src; if (typeof modal.showModal === 'function') modal.showModal(); else window.open(src, '_blank', 'noopener'); }); document.addEventListener('click', function (event) { if (!modal.open) return; if (event.target === modal) modal.close(); }); modal.addEventListener('close', function () { modalImg.src = ''; }); } /* ---- DYNAMIC ISLAND HEADER ---- */ function initDynamicIslandHeader() { var masthead = document.querySelector('.masthead'); if (!masthead) return; var ticking = false; function update() { ticking = false; document.documentElement.classList.toggle('island-scrolled', window.scrollY > 24); } window.addEventListener('scroll', function () { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true }); update(); } /* ---- DELETE SOUND ---- */ function initDeleteSound() { document.addEventListener('submit', function (e) { var form = e.target; if (!form || !form.action) return; if (!/\/(delete|destroy)$/.test(form.action)) return; try { AudioFX.delete(); } catch (err) {} }, true); } /* ---- REALTIME PULSE ---- */ function initRealtime() { if (!document.body) return; var userId = parseInt(document.body.getAttribute('data-user-id') || '0', 10); if (userId <= 0) return; var DEBUG = /[?&]pulse_debug=1/.test(location.search); var pulseUrlPrimary = resolveUrl('api/pulse'); var pulseUrlFallback = resolveUrl('index.php/api/pulse'); var useFallback = false; var state = { latestNotifId: parseInt(document.body.getAttribute('data-latest-notif-id') || '0', 10), latestDmId: parseInt(document.body.getAttribute('data-latest-dm-id') || '0', 10), notifReady: false, dmReady: false, active: true, busy: false, busySince: null, consecutiveErrors: 0 }; var debugEl = null; if (DEBUG) { debugEl = document.createElement('div'); debugEl.id = 'pulse-debug-panel'; debugEl.style.cssText = 'position:fixed;right:6px;bottom:6px;z-index:2147483647;background:rgba(0,0,0,0.88);color:#0f0;font:11px/1.35 ui-monospace,Menlo,monospace;padding:6px 8px;border-radius:6px;max-width:300px;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-all;pointer-events:auto;'; debugEl.textContent = 'pulse v6\n'; document.body.appendChild(debugEl); } function dbg(msg) { try { console.log('[pulse]', msg); } catch (e) {} if (debugEl) { var line = document.createElement('div'); var t = new Date(); line.textContent = ('0' + t.getHours()).slice(-2) + ':' + ('0' + t.getMinutes()).slice(-2) + ':' + ('0' + t.getSeconds()).slice(-2) + ' ' + msg; debugEl.appendChild(line); while (debugEl.childNodes.length > 40) debugEl.removeChild(debugEl.firstChild); debugEl.scrollTop = debugEl.scrollHeight; } } function updateBadge(anchorEl, count, forcePulse) { if (!anchorEl) return; var existing = anchorEl.querySelectorAll('[data-badge-el]'); if (count <= 0) { existing.forEach(function (el) { el.remove(); }); return; } var badge = existing[0]; if (existing.length > 1) { for (var i = 1; i < existing.length; i++) existing[i].remove(); } if (!badge) { badge = document.createElement('span'); badge.className = 'island-icon-badge'; badge.setAttribute('data-badge-el', ''); badge.setAttribute('aria-hidden', 'true'); anchorEl.appendChild(badge); } var newText = count > 99 ? '99+' : String(count); var changed = badge.textContent !== newText; badge.textContent = newText; if (changed || forcePulse) { badge.classList.remove('is-new'); void badge.offsetWidth; badge.classList.add('is-new'); setTimeout(function () { badge.classList.remove('is-new'); }, 520); } } function updateWalletBalance(newBalance) { var els = document.querySelectorAll('[data-wallet-balance]'); els.forEach(function (el) { var oldBalance = parseInt(el.getAttribute('data-wallet-balance'), 10) || 0; if (oldBalance === newBalance) return; var symbol = el.getAttribute('data-wallet-symbol') || 'Ꞩ'; el.textContent = symbol + newBalance.toLocaleString('en-US'); el.setAttribute('data-wallet-balance', String(newBalance)); el.classList.remove('is-updated-credit', 'is-updated-debit'); void el.offsetWidth; el.classList.add(newBalance > oldBalance ? 'is-updated-credit' : 'is-updated-debit'); setTimeout(function () { el.classList.remove('is-updated-credit', 'is-updated-debit'); }, 1700); if (newBalance > oldBalance) { try { AudioFX.receive(); } catch (e) {} try { TitleFlash.flash('Saldo bertambah — Sochouse'); } catch (e) {} } }); } function applyPayload(data) { if (!data || data.ok !== true || data.authenticated !== true) return; var nUnread = parseInt(data.notif_unread, 10); if (isNaN(nUnread)) nUnread = 0; var dUnread = parseInt(data.dm_unread, 10); if (isNaN(dUnread)) dUnread = 0; var newNotif = (data.latest_notif_id | 0) > state.latestNotifId; var newDm = (data.latest_dm_id | 0) > state.latestDmId; try { updateBadge(document.getElementById('badge-notif'), nUnread, newNotif && nUnread > 0); } catch (e) {} try { updateBadge(document.getElementById('badge-dm'), dUnread, newDm && dUnread > 0); } catch (e) {} if (state.notifReady && newNotif && nUnread > 0 && !/\/notifications/.test(location.pathname)) { try { AudioFX.notify(); } catch (e) {} try { TitleFlash.flash('Notifikasi baru'); } catch (e) {} } if (state.dmReady && newDm && dUnread > 0 && !document.getElementById('dm-chat') && !/^\/dm\//.test(location.pathname) && !data.latest_dm_is_muted) { try { AudioFX.receive(); } catch (e) {} try { TitleFlash.flash('Pesan baru'); } catch (e) {} } state.latestNotifId = data.latest_notif_id | 0; state.latestDmId = data.latest_dm_id | 0; state.notifReady = true; state.dmReady = true; if (typeof data.wallet_balance === 'number') { try { updateWalletBalance(data.wallet_balance); } catch (e) {} } state.consecutiveErrors = 0; } function pulse() { if (!state.active || state.busy) return; state.busy = true; state.busySince = Date.now(); var base = useFallback ? pulseUrlFallback : pulseUrlPrimary; var sep = base.indexOf('?') >= 0 ? '&' : '?'; var url = base + sep + '_t=' + Date.now(); var finished = function () { state.busy = false; state.busySince = null; }; try { fetch(url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Cache-Control': 'no-cache, no-store, must-revalidate' }, credentials: 'same-origin', cache: 'no-store' }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text().then(function (text) { if (!text) throw new Error('Empty'); try { return JSON.parse(text); } catch (e) { throw new Error('Non-JSON'); } }); }) .then(applyPayload) .catch(function (err) { state.consecutiveErrors++; dbg('ERR #' + state.consecutiveErrors + ': ' + (err && err.message ? err.message : err)); if (state.consecutiveErrors === 3 && !useFallback) useFallback = true; }) .then(finished, finished); } catch (e) { finished(); } } setTimeout(pulse, 200); setInterval(function () { if (state.active && !state.busy) pulse(); }, 1200); setInterval(function () { if (state.busy && state.busySince && (Date.now() - state.busySince) > 8000) { state.busy = false; state.busySince = null; } }, 3000); document.addEventListener('visibilitychange', function () { state.active = !document.hidden; if (state.active) pulse(); }); window.addEventListener('pageshow', function (e) { if (e.persisted) { state.busy = false; state.busySince = null; pulse(); } }); window.__pulseNow = function () { state.busy = false; pulse(); }; } /* ============ EXPORT KE window.__sochouse ============ */ window.__sochouse = { resolveUrl: resolveUrl, getLoginUrl: getLoginUrl, getCsrfValue: getCsrfValue, fetchJson: fetchJson, showStoryToast: showStoryToast, AudioFX: AudioFX, TitleFlash: TitleFlash }; window.AudioFX = AudioFX; window.TitleFlash = TitleFlash; /* ============ INIT ============ */ function init() { try { initAvatarModal(); } catch (e) { console.error('initAvatarModal', e); } try { initDynamicIslandHeader(); } catch (e) { console.error('initDynamicIslandHeader', e); } try { initDeleteSound(); } catch (e) { console.error('initDeleteSound', e); } try { initRealtime(); } catch (e) { console.error('initRealtime', e); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();