1/* WWSTATCOL: a fourth column to the right of the feed and the existing sidebar. 2 Injected here rather than in the .phtml because the row is assembled across 3 several templates (home/content.phtml opens it, sidebar/content.phtml floats the 4 right column inside a full-width wrapper) - adding a sibling server-side means 5 editing all of them, while the card's contents are fetched over AJAX anyway. 6 First card: Weekly Stats, cycling through five metrics with week-on-week change. */ 7(function () { 8 if (window.__wwStatCol) { return; } 9 window.__wwStatCol = 1; 10 11 var ROT = 4500; 12 /* Module level, so a rebuilt column cannot leave the old card's rotation 13 ticking against a detached node. */ 14 var statTimer = null; 15 16 function css() { 17 if (document.getElementById('ww-statcol-css')) { return; } 18 var s = document.createElement('style'); 19 s.id = 'ww-statcol-css'; 20 s.textContent = [ 21 '.ww-statcol{float:left;padding:0 8px 0 0;min-height:1px}', 22 '@media (min-width:1280px){html body .row.wo_user_profile > .col-md-8{width:52% !important;zoom:.9}html body .row.wo_user_profile > .col-md-4{width:31% !important}html body .row.wo_user_profile > .ww-statcol{width:17% !important;padding:0 0 0 8px}}', 23 '.ww-wd-btns{display:flex;gap:6px}', 24 '.ww-wd-btns a{flex:1 1 0;min-width:0;font-size:11px;font-weight:600;line-height:1;white-space:nowrap;padding:7px 4px;border-radius:7px;text-decoration:none;text-align:center;transition:background .15s,color .15s,border-color .15s}', 25 '.ww-wd-dash{color:#ff7a7a;background:linear-gradient(180deg,rgba(255,45,45,.22),rgba(255,45,45,.12));border:1px solid rgba(255,45,45,.38)}', 26 '.ww-wd-dash:hover{background:linear-gradient(180deg,rgba(255,45,45,.34),rgba(255,45,45,.20));border-color:rgba(255,45,45,.55);color:#ff9d9d;text-decoration:none}', 27 '.ww-wd-studio{color:#dcdce4;background:linear-gradient(180deg,rgba(255,255,255,.16),rgba(255,255,255,.07));border:1px solid rgba(255,255,255,.22)}', 28 '.ww-wd-studio:hover{background:linear-gradient(180deg,rgba(255,255,255,.26),rgba(255,255,255,.13));border-color:rgba(255,255,255,.34);color:#fff;text-decoration:none}', 29 '.ww-upcard{background:linear-gradient(180deg,rgba(245,197,24,.20),rgba(245,197,24,.07));border:1px solid rgba(245,197,24,.42);border-radius:var(--ww-card-radius,12px);box-shadow:var(--ww-card-shadow,0 1px 2px rgba(0,0,0,.45));padding:11px 11px 12px;margin-bottom:14px;display:none}', 30 '.ww-upcard.on{display:block}', 31 '.ww-upcard h4{margin:0 0 8px;font-size:11px;font-weight:700;letter-spacing:.3px;text-transform:uppercase;color:#f7cf3a}', 32 '.ww-up-thumb{display:block;width:calc(100% + 22px);max-width:none;margin:0 -11px;aspect-ratio:16/9;height:auto;object-fit:cover;border-radius:0;border-top:1px solid rgba(245,197,24,.42);border-bottom:1px solid rgba(245,197,24,.42);background:rgba(0,0,0,.4)}', 33 '.ww-up-title{margin:9px 0 3px;font-size:11px;font-weight:600;line-height:1.3;color:#f2f2f5;text-decoration:none;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}', 34 '.ww-up-title:hover{color:#fff;text-decoration:none}', 35 '.ww-up-when{display:block;font-size:10.5px;font-weight:600;color:#f7cf3a}', 36 '.ww-up-list{display:flex;flex-direction:column;gap:14px;margin:0 0 12px}', 37 '.ww-up-item{display:block;text-decoration:none}', 38 '.ww-up-item:hover b{color:#fff}', 39 '.ww-up-item img{display:block;width:calc(100% + 22px);max-width:none;margin:0 -11px;aspect-ratio:16/9;height:auto;object-fit:cover;border-radius:0;border-top:1px solid rgba(245,197,24,.42);border-bottom:1px solid rgba(245,197,24,.42);background:rgba(0,0,0,.4)}', 40 '.ww-up-meta{display:block;min-width:0;margin-top:7px}', 41 '.ww-up-meta b{font-size:11px;font-weight:600;line-height:1.3;color:#f2f2f5;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}', 42 '.ww-up-meta span{display:block;margin-top:3px;font-size:10px;font-weight:600;color:#f7cf3a}', 43 '.ww-up-none{font-size:10.5px;color:#d8cfa8;margin:0 0 10px}', 44 '.ww-up-btn{display:block;text-align:center;font-size:11px;font-weight:600;line-height:1;padding:7px 4px;border-radius:7px;text-decoration:none;color:#f7cf3a;background:linear-gradient(180deg,rgba(245,197,24,.22),rgba(245,197,24,.10));border:1px solid rgba(245,197,24,.42);transition:background .15s,color .15s,border-color .15s}', 45 '.ww-up-btn:hover{background:linear-gradient(180deg,rgba(245,197,24,.34),rgba(245,197,24,.18));color:#ffdf6b;border-color:rgba(245,197,24,.6);text-decoration:none}', 46 '.ww-statcard{background:var(--ww-card-bg,rgba(20,20,20,.6));border:1px solid var(--ww-card-line,rgba(255,255,255,.1));border-radius:var(--ww-card-radius,12px);box-shadow:var(--ww-card-shadow,0 1px 2px rgba(0,0,0,.45));overflow:hidden}', 47 '.ww-statcard-h{display:block;padding:9px 10px;border-bottom:1px solid rgba(255,255,255,.07);color:#e8e8e8}', 48 '.ww-stat-row{display:flex;align-items:center;justify-content:space-between;gap:5px}', 49 '.ww-stat-row b{font-size:11.5px;font-weight:700;white-space:nowrap;letter-spacing:-.2px}', 50 '.ww-statcard-h .ww-sub{display:block;margin-top:2px;white-space:nowrap}', 51 '.ww-stat-more{flex:0 0 auto;font-size:9.5px;font-weight:600;line-height:1;white-space:nowrap;text-decoration:none;color:#ff7a7a;background:linear-gradient(180deg,rgba(255,45,45,.22),rgba(255,45,45,.12));border:1px solid rgba(255,45,45,.38);border-radius:7px;padding:5px 5px;letter-spacing:-.2px;transition:background .15s,color .15s,border-color .15s}', 52 '.ww-stat-more:hover{background:linear-gradient(180deg,rgba(255,45,45,.34),rgba(255,45,45,.20));border-color:rgba(255,45,45,.55);color:#ff9d9d;text-decoration:none}', 53 '.ww-statcard-h .ww-sub{font-size:11px;font-weight:500;color:#8a8a8a}', 54 '.ww-stat-body{position:relative;padding:16px 14px 12px;min-height:132px;text-align:center}', 55 '.ww-stat-slide{display:none}', 56 '.ww-stat-slide.on{display:block}', 57 '.ww-stat-lab{font-size:12px;letter-spacing:.6px;text-transform:uppercase;color:#8a8a8a;margin-bottom:6px}', 58 '.ww-stat-val{font-size:30px;font-weight:800;color:#fff;line-height:1.1}', 59 '.ww-stat-chg{display:inline-flex;align-items:center;gap:5px;margin-top:9px;font-size:12px;font-weight:700;padding:3px 9px;border-radius:20px}', 60 '.ww-stat-chg.up{color:#4caf50;background:rgba(76,175,80,.12)}', 61 '.ww-stat-chg.down{color:#e53935;background:rgba(229,57,53,.12)}', 62 '.ww-stat-chg.flat{color:#8a8a8a;background:rgba(255,255,255,.06)}', 63 '.ww-stat-prev{margin-top:7px;font-size:11px;color:#7a7a7a}', 64 '.ww-stat-dots{display:flex;justify-content:center;gap:6px;padding:4px 0 10px}', 65 '.ww-stat-dots i{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.22);cursor:pointer;transition:background .15s}', 66 '.ww-stat-dots i.on{background:#e53935}', 67 '.ww-stat-empty{color:#8a8a8a;font-size:12px;padding:16px 0}', 68 '.ww-folcard{display:none;margin-top:14px}', 69 '.ww-folcard.on{display:block}', 70 '.ww-fol-body{padding:5px 0 1px}', 71 '.ww-fol-page{display:none}', 72 '.ww-fol-page.on{display:block}', 73 '.ww-fol-item{display:flex;align-items:center;gap:8px;padding:5px 10px;text-decoration:none}', 74 '.ww-fol-item:hover{background:rgba(255,255,255,.05);text-decoration:none}', 75 '.ww-fol-av{position:relative;flex:0 0 auto;width:28px;height:28px}', 76 '.ww-fol-av img{display:block;width:28px;height:28px;border-radius:50%;object-fit:cover;background:rgba(255,255,255,.06)}', 77 '.ww-fol-meta{flex:1 1 auto;min-width:0}', 78 '.ww-fol-line{display:flex;align-items:center;min-width:0;font-size:11.5px;font-weight:600;line-height:1.25;color:#e8e8e8}', 79 '.ww-fol-line > *{flex:0 0 auto}', 80 '.ww-fol-nm{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', 81 '.ww-fol-tags{display:inline-flex;align-items:center}', 82 '.ww-folcard span.ww-role-badge{font-size:8px;padding:0 3px;line-height:12px;margin-right:4px;letter-spacing:0}', 83 '.ww-folcard .ww-live-badge{font-size:8px;padding:1px 4px;margin-left:4px}', 84 '.ww-folcard svg.ww-live-check{width:11px;height:11px;margin-left:3px}', 85 '.ww-fol-stat{display:flex;align-items:center;gap:5px;margin-top:2px;font-size:10px;font-weight:600;line-height:1;white-space:nowrap}', 86 '.ww-fol-stat i{flex:0 0 auto;font-size:11px;line-height:1}', 87 '.ww-fol-empty{padding:14px 10px;text-align:center;color:#8a8a8a;font-size:12px}', 88 '.ww-fol-dots{display:flex;justify-content:center;gap:6px;padding:6px 0 10px}', 89 '.ww-fol-dots:empty{display:none}', 90 '.ww-fol-dots i{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.22);cursor:pointer;transition:background .15s}', 91 '.ww-fol-dots i.on{background:#e53935}' 92 ].join('\n'); 93 document.head.appendChild(s); 94 } 95 96 function esc(t) { return String(t == null ? '' : t).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } 97 98 function nfmt(n) {
99 n = Number(n) || 0; 100 if (n >= 1000000) { return (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1) + 'M'; } 101 if (n >= 1000) { return (n / 1000).toFixed(n % 1000 === 0 ? 0 : 1) + 'K'; } 102 return (Math.round(n * 10) / 10).toString(); 103 } 104 105 function render(host, rows) { 106 if (!rows || !rows.length) { 107 host.innerHTML = '<div class="ww-stat-empty">No activity yet this week.</div>'; 108 return; 109 } 110 var body = '', dots = ''; 111 for (var i = 0; i < rows.length; i++) { 112 var r = rows[i]; 113 var cls = 'flat', arrow = '·', txt = 'no change'; 114 if (r.pct === null || r.pct === undefined) { txt = 'no data last week'; } 115 else if (r.pct > 0) { cls = 'up'; arrow = '▲'; txt = r.pct + '%'; } 116 else if (r.pct < 0) { cls = 'down'; arrow = '▼'; txt = Math.abs(r.pct) + '%'; } 117 else { txt = '0%'; } 118 body += '<div class="ww-stat-slide' + (i === 0 ? ' on' : '') + '">' + 119 '<div class="ww-stat-lab">' + esc(r.label) + '</div>' + 120 '<div class="ww-stat-val">' + nfmt(r.value) + '</div>' + 121 '<div class="ww-stat-chg ' + cls + '">' + arrow + ' ' + txt + '</div>' + 122 '<div class="ww-stat-prev">' + nfmt(r.prev) + ' last week</div>' + 123 '</div>'; 124 dots += '<i data-i="' + i + '"' + (i === 0 ? ' class="on"' : '') + '></i>'; 125 } 126 host.innerHTML = body; 127 var dotWrap = host.parentNode.querySelector('.ww-stat-dots'); 128 dotWrap.innerHTML = dots; 129 130 var at = 0; 131 clearInterval(statTimer); 132 function show(n) { 133 var slides = host.querySelectorAll('.ww-stat-slide'); 134 var ds = dotWrap.querySelectorAll('i'); 135 at = (n + slides.length) % slides.length; 136 for (var k = 0; k < slides.length; k++) { 137 slides[k].className = 'ww-stat-slide' + (k === at ? ' on' : ''); 138 if (ds[k]) { ds[k].className = (k === at ? 'on' : ''); } 139 } 140 } 141 function play() { clearInterval(statTimer); statTimer = setInterval(function () { show(at + 1); }, ROT); } 142 143 dotWrap.addEventListener('click', function (e) { 144 var d = e.target.getAttribute ? e.target.getAttribute('data-i') : null; 145 if (d === null) { return; } 146 show(parseInt(d, 10)); play(); 147 }); 148 var card = host.closest ? host.closest('.ww-statcard') : null; 149 if (card) { 150 card.addEventListener('mouseenter', function () { clearInterval(statTimer); }); 151 card.addEventListener('mouseleave', play); 152 } 153 play(); 154 } 155 156 157 function wwUpEsc(s) { 158 return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) { 159 return { "&": "&", "<": "<", ">": ">", "\"": """ }[c]; 160 }); 161 } 162 163 function wwUpWhen(ts) { 164 var d = new Date(ts * 1000); 165 var mo = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][d.getMonth()]; 166 var h = d.getHours(), ap = h >= 12 ? "PM" : "AM"; 167 h = h % 12; if (!h) { h = 12; } 168 var mi = d.getMinutes(); mi = mi < 10 ? "0" + mi : "" + mi; 169 return mo + " " + d.getDate() + ", " + h + ":" + mi + " " + ap; 170 } 171 172 function wwUpcoming() { 173 var host = document.getElementById("ww-upcard"); 174 if (!host) { return; } 175 var x = new XMLHttpRequest(); 176 x.open("GET", "/requests.php" + "?f=ww_upcoming", true); 177 x.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 178 x.onload = function () { 179 var j; 180 try { j = JSON.parse(x.responseText); } catch (e) { return; } 181 if (!j || j.status !== 200) { return; } 182 var h = "", i, o; 183 if (j.mine) { 184 h = "<h4>Upcoming Stream</h4>" + 185 "<a href=\"/livenow\"><img class=\"ww-up-thumb\" src=\"" + wwUpEsc(j.mine.thumb) + "\" alt=\"\" onerror=\"this.style.opacity=.25\"></a>" + 186 "<a class=\"ww-up-title\" href=\"/livenow\">" + wwUpEsc(j.mine.title) + "</a>" + 187 "<span class=\"ww-up-when\">" + wwUpWhen(j.mine.at) + "</span>"; 188 } else if (j.others && j.others.length) { 189 h = "<h4>Upcoming Streams</h4><div class=\"ww-up-list\">"; 190 for (i = 0; i < j.others.length; i++) { 191 o = j.others[i]; 192 h += "<a class=\"ww-up-item\" href=\"/livenow\">" + 193 "<img src=\"" + wwUpEsc(o.thumb) + "\" alt=\"\" onerror=\"this.style.opacity=.25\">" + 194 "<span class=\"ww-up-meta\"><b>" + wwUpEsc(o.title) + "</b><span>" + wwUpWhen(o.at) + 195 (o.user ? " ·
195 " + wwUpEsc(o.user) : "") + "</span></span></a>"; 196 } 197 h += "</div><a class=\"ww-up-btn\" href=\"/schedule-stream\">Schedule Stream</a>"; 198 } else { 199 h = "<h4>Upcoming Streams</h4><div class=\"ww-up-none\">Nothing scheduled yet.</div>" + 200 "<a class=\"ww-up-btn\" href=\"/schedule-stream\">Schedule Stream</a>"; 201 } 202 host.innerHTML = h; 203 host.className = "ww-upcard on"; 204 }; 205 x.send(); 206 } 207 208 /* ---------------------------------------------------------------- Following card 209 Everyone the viewer follows, five at a time, paging every 5s. The row markup is 210 shaped so ww-role-badges.js decorates it for free: the anchor points at /username 211 and carries a .sidebar-listed-user-name, which is exactly what its profile-link 212 pass looks for, so staff badges stay defined in one place. The verified tick and 213 the LIVE badge are ours because that pass only reads usernames. */ 214 var FOL_PER = 5, FOL_ROT = 5000, FOL_REFRESH = 120000; 215 var FOL_COLOR = { live: '#ff3b30', idle: '#ffc107', online: '#60d465', offline: '#CDCDCD' }; 216 var FOL_LABEL = { live: 'Live', idle: 'Idle', online: 'Online', offline: 'Offline' }; 217 var FOL_ICON = { android: 'fa-android', ios: 'fa-apple', desktop: 'fa-desktop' }; 218 var FOL_DEV = { android: 'Android', ios: 'iPhone', desktop: 'Web' }; 219 var FOL_CHECK = '<svg class="ww-live-check" viewBox="0 0 24 24">' + 220 '<circle cx="12" cy="12" r="11" fill="#1d9bf0"></circle>' + 221 '<path d="M17.2 9.1l-6 6-3-3 1.4-1.4 1.6 1.6 4.6-4.6z" fill="#fff"></path>' + 222 '</svg>'; 223 224 var folTimer = null, folRefresh = null, folAt = 0, folVer = null; 225 226 function folVerified(id) { 227 if (folVer === null) { 228 folVer = {}; 229 var v = window.WW_VERIFIED_IDS || []; 230 for (var i = 0; i < v.length; i++) { folVer[String(v[i])] = 1; } 231 } 232 return folVer[String(id)] === 1; 233 } 234 235 function folRow(u) { 236 var st = FOL_LABEL[u.state] ? u.state : 'offline'; 237 var dev = FOL_ICON[u.device] ? u.device : ''; 238 var txt = FOL_LABEL[st] + (dev ? ' \u00b7 ' + FOL_DEV[dev] : ''); 239 return '<a class="ww-fol-item" href="/' + encodeURIComponent(u.user) + '" title="' + esc(u.name) + ' \u2014 ' + txt + '">' + 240 '<span class="ww-fol-av' + (st === 'live' ? ' ww-live-ring' : '') + '">' + 241 '<img src="' + esc(u.avatar) + '" alt="" onerror="this.style.visibility=\'hidden\'">' + 242 '</span>' + 243 '<span class="ww-fol-meta">' + 244 '<span class="ww-fol-line sidebar-listed-user-name">' + 245 '<span class="ww-fol-nm">' + esc(u.name) + '</span>' + 246 '<span class="ww-fol-tags">' + 247 (folVerified(u.id) ? FOL_CHECK : '') + 248 (st === 'live' ? '<span class="ww-live-badge" title="Live now">LIVE</span>' : '') + 249 '</span>' + 250 '</span>' + 251 '<span class="ww-fol-stat" style="color:' + FOL_COLOR[st] + '">' + 252 (dev ? '<i class="fa ' + FOL_ICON[dev] + '"></i>' : '') + 253 '<span>' + txt + '</span>' + 254 '</span>' + 255 '</span>' + 256 '</a>'; 257 } 258 259 function folRender(host, users) { 260 var pages = [], i, k; 261 for (i = 0; i < users.length; i += FOL_PER) { pages.push(users.slice(i, i + FOL_PER)); } 262 263 var body = '', dots = ''; 264 for (i = 0; i < pages.length; i++) { 265 var rows = ''; 266 for (k = 0; k < pages[i].length; k++) { rows += folRow(pages[i][k]); } 267 body += '<div class="ww-fol-page">' + rows + '</div>'; 268 dots += '<i data-i="' + i + '"></i>'; 269 } 270 host.innerHTML = body; 271 272 var dotWrap = host.parentNode.querySelector('.ww-fol-dots'); 273 dotWrap.innerHTML = (pages.length > 1) ? dots : ''; 274 275 clearInterval(folTimer); 276 if (folAt >= pages.length) { folAt = 0; } /* a refresh may have shortened the list */ 277 278 function show(n) { 279 var slides = host.querySelectorAll('.ww-fol-page'); 280 var ds = dotWrap.querySelectorAll('i'); 281 folAt = (n + slides.length) % slides.length; 282 for (var j = 0; j < slides.length; j++) { 283 slides[j].className = 'ww-fol-page' + (j === folAt ? ' on' : ''); 284 if (ds[j]) { ds[j].className = (j === folAt ? 'on' : ''); } 285 } 286 } 287 function play() { 288 clearInterval(folTimer); 289 if (pages.length < 2) { return; } 290 folTimer = setInterval(function () { show(folAt + 1); }, FOL_ROT); 291 } 292 show(folAt); 293 294 if (!dotWrap.getAttribute('data-ww-bound')) { 295 dotWrap.setAttribute('data-ww-bound', '1'); 296 dotWrap.addEventListener('click', function (e) { 297 var d = e.target.getAttribute ? e.target.getAttribute('data-i') : null; 298 if (d === null) { return; } 299 show(parseInt(d, 10)); play(); 300 }); 301 var card = document.getElementById('ww-folcard'); 302 if (card) { 303 card.addEventListener('mouseenter', function () { clearInterval(folTimer); }); 304 card.addEventListener('mouseleave', play); 305 } 306 } 307 play(); 308 } 309 310 function wwFollowing() { 311 var card = document.getElementById('ww-folcard'); 312 if (!card) { return; } 313 var body = card.querySelector('.ww-fol-body'); 314 var x = new XMLHttpRequest(); 315 x.open('GET', '/requests.php?f=ww_following', true); 316 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 317 x.onload = function () { 318 var j;
319 try { j = JSON.parse(x.responseText); } catch (e) { return; } 320 if (!j || j.status !== 200 || !j.users) { return; } 321 /* An empty list is a real state now that offline rows are dropped, and a refresh 322 can land on it - so empty means hide the card, not leave the old list standing. */ 323 if (!j.users.length) { 324 clearInterval(folTimer); 325 folTimer = null; 326 card.className = 'ww-statcard ww-folcard'; 327 return; 328 } 329 var all = card.querySelector('#ww-fol-all'); 330 if (all && j.me) { all.setAttribute('href', '/' + encodeURIComponent(j.me) + '/following'); } 331 var sub = card.querySelector('#ww-fol-sub'); 332 if (sub) { sub.innerHTML = nfmt(j.online) + ' online now'; } 333 folRender(body, j.users); 334 card.className = 'ww-statcard ww-folcard on'; 335 }; 336 x.send(); 337 } 338 339 function build() { 340 /* members only - <body> carries ww-guest when logged out */ 341 if (document.body && document.body.classList.contains('ww-guest')) { return; } 342 343 var row = wwStatMount(); 344 if (!row) { return; } 345 if (row.querySelector('.ww-statcol')) { return; } 346 347 css(); 348 var col = document.createElement('div'); 349 col.className = 'ww-statcol'; 350 col.innerHTML = 351 /* WW-WDCARD-REMOVED */ 352 '<div class="ww-upcard" id="ww-upcard"></div>' + 353 '<div class="ww-statcard">' + 354 '<div class="ww-statcard-h">' + 355 '<div class="ww-stat-row"><b>Weekly Stats</b><a class="ww-stat-more" href="/dashboard?page=stats-analytics">More Stats</a></div>' + 356 '<span class="ww-sub">vs last week</span>' + 357 '</div>' + 358 '<div class="ww-stat-body"><div class="ww-stat-empty">Loading…</div></div>' + 359 '<div class="ww-stat-dots"></div>' + 360 '</div>' + 361 '<div class="ww-statcard ww-folcard" id="ww-folcard">' + 362 '<div class="ww-statcard-h">' + 363 '<div class="ww-stat-row"><b>Following</b><a class="ww-stat-more" id="ww-fol-all" href="/">View All</a></div>' + 364 '<span class="ww-sub" id="ww-fol-sub">Loading…</span>' + 365 '</div>' + 366 '<div class="ww-fol-body"><div class="ww-fol-empty">Loading…</div></div>' + 367 '<div class="ww-fol-dots"></div>' + 368 '</div>'; 369 row.appendChild(col); 370 wwUpcoming(); 371 wwFollowing(); 372 /* One refresh loop for the life of the page - presence goes stale after 2 min - 373 even if the column itself gets rebuilt underneath it. */ 374 if (folRefresh === null) { folRefresh = setInterval(wwFollowing, FOL_REFRESH); } 375 376 var body = col.querySelector('.ww-stat-body'); 377 var xhr = new XMLHttpRequest(); 378 xhr.open('GET', '/requests.php?f=ww_weekly_stats', true); 379 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 380 xhr.onload = function () { 381 try { 382 var j = JSON.parse(xhr.responseText); 383 render(body, j && j.data); 384 } catch (e) { 385 body.innerHTML = '<div class="ww-stat-empty">Stats unavailable.</div>'; 386 } 387 }; 388 xhr.onerror = function () { body.innerHTML = '<div class="ww-stat-empty">Stats unavailable.</div>'; }; 389 xhr.send(); 390 } 391 392 /* WWSTATCOL-BOOT: the column used to be built on DOMContentLoaded and window load and 393 nowhere else, so a single miss was permanent - if the feed row was not in place at 394 either of those two instants, nothing ever looked again and the column stayed gone 395 until the user reloaded by hand. Keep trying on a bounded schedule instead, and stop 396 the moment the column is up. */ 397 var BOOT_EVERY = 400, BOOT_FOR = 20000; 398 var bootUntil = 0, bootTimer = null; 399 400 /* WWSTATCOL-MOUNT: the single answer to "where does the column go on the page 401 as it stands right now". 402 - waves supplies its own host, because that page has no feed row at all 403 (the stage is a fixed full-viewport overlay) 404 - everywhere else, anchor on the feed column and walk up to its row rather 405 than taking the FIRST .row in the container: a header ad or promo above 406 the layout row used to win that query and the column was skipped 407 - profile pages resolve to null on purpose, so the timeline keeps the width */ 408 function wwStatMount() { 409 var h = document.getElementById('ww-waves-statcol'); 410 if (h) { return h; } 411 var mid = document.querySelector('.content-container .row .col-md-7.middlecol'); 412 if (!mid) { return null; } 413 return mid.closest ? mid.closest('.row') : mid.parentNode; 414 } 415 416 /* WWSTATCOL-SPA: ask whether THIS page's mount holds a column, not whether one 417 exists anywhere in the document. The old global check is what made the column 418 miss when you clicked Waves in the sidebar: the site navigates by ajax, so the 419 home feed's column was still in memory when build() was asked again, built() 420 said "yes" and the boot loop shut down for the life of the tab. A hard load of 421 the same URL worked, which is exactly the "only when I click it from the 422 sidebar" shape of the bug. */ 423 function built() { 424 var m = wwStatMount(); 425 return !!(m && m.querySelector('.ww-statcol')); 426 } 427 428 function kick() { 429 build(); 430 if (built()) { 431 if (bootTimer) { clearInterval(bootTimer); bootTimer = null; } 432 return; 433 } 434 bootUntil = Date.now() + BOOT_FOR; 435 if (bootTimer) { return; } 436 bootTimer = setInterval(function () { 437 build(); 438 if (built() || Date.now() > bootUntil) { clearInterval(bootTimer); bootTimer = null; } 439 }, BOOT_EVERY); 440 } 441 442 if (document.readyState === 'loading') { 443 document.addEventListener('DOMContentLoaded', kick); 444 } else { kick(); } 445 window.addEventListener('load', kick); 446 447 /* WWSTATCOL-SPA: kick again whenever the app navigates without a page load. 448 In-app links go through pushState, so wrapping that (plus popstate for 449 back/forward) covers every route change. kick() no-ops when this page's mount 450 already holds a column, and it brings its own retry loop, which absorbs the 451 gap between the URL changing and the new content actually landing. */ 452 ['pushState', 'replaceState'].forEach(function (m) { 453 var orig = history[m]; 454 if (typeof orig !== 'function') { return; } 455 history[m] = function () { 456 var r = orig.apply(this, arguments);
457 try { setTimeout(kick, 0); } catch (e) {} 458 return r; 459 }; 460 }); 461 window.addEventListener('popstate', function () { setTimeout(kick, 0); }); 462})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.