PageSourceSearch

https://worldswave.com/themes/wowonder/javascript/ww-chat-pin.js?v=ww8

js worldswave.com collected 2026-10-02 15:34:00 UTC 9,078 bytes, 213 lines download raw bytes

1/* ww-chat-pin.js - pinned chat message for WaveDeck live chat.
2 * Draws a pin banner above the chat list, and a hover "..." menu on each
3 * message for the host, their moderators, and site admins.
4 * All permission and storage logic lives in /wavedeck_pin.php - this file
5 * only draws what that endpoint allows. Viewers get the banner, no controls. */
6(function () {
7  var ROW = '.live-now-hero-chat-msg';
8  var SID = 0;
9  /* The studio sets window.WW_WC_SID; the public watch page (livenow) does not,
10     but renders data-ww-stream-id on the chat container. Read whichever exists.
11     Never parse the URL slug - slugs are titles, not ids. */
12  function getSID() {
13    if (SID) { return SID; }
14    var w = parseInt(window.WW_WC_SID || 0, 10);
15    if (w) { SID = w; return SID; }
16    var b = findBox();
17    var host = (b && b.closest) ? b.closest('[data-ww-stream-id]') : null;
18    if (!host) { host = document.querySelector('[data-ww-stream-id]'); }
19    if (host) { SID = parseInt(host.getAttribute('data-ww-stream-id'), 10) || 0; }
20    return SID;
21  }
22  var CSRF = null, CAN = false, PIN = null, banner = null;
23
24  var css = '.wwpin-bar{display:flex;gap:8px;align-items:flex-start;padding:8px 12px;margin:6px 8px;'
25    + 'border-radius:6px;background:rgba(0,0,0,.5);border:1px solid rgba(255,45,45,.9);'
26    + 'box-shadow:0 0 10px rgba(255,45,45,.65),inset 0 0 10px rgba(255,45,45,.18);'
27    + 'font-size:13px;line-height:1.4;max-height:176px;overflow-y:auto;'
28    + 'scrollbar-width:thin;flex:0 0 auto;color:#e8e8e8}'
29    + '.wwpin-bar[hidden]{display:none}'
30    + '.wwpin-ico{flex:0 0 auto}'
31    + '.wwpin-body{flex:1 1 auto;min-width:0;overflow-wrap:anywhere;word-break:break-word}'
32    + '.wwpin-who{font-weight:600;color:#ff7b7b;margin-right:4px}'
33    + '.wwpin-x{margin-left:auto;cursor:pointer;opacity:.6;background:none;border:0;color:#fff;'
34    + 'font-size:16px;line-height:1;padding:0 2px}'
35    + '.wwpin-x:hover{opacity:1}'
36    + ROW + '{position:relative}'
37    + '.wwpin-dots{position:absolute;top:0;right:2px;opacity:0;cursor:pointer;'
38    + 'background:rgba(0,0,0,.55);border:0;color:#ddd;border-radius:4px;padding:0 6px;'
39    + 'font-size:14px;line-height:18px}'
40    + ROW + ':hover .wwpin-dots{opacity:1}'
41    + '.wwpin-menu{position:absolute;top:20px;right:2px;z-index:60;background:#15171c;'
42    + 'border:1px solid #333;border-radius:6px;box-shadow:0 6px 20px rgba(0,0,0,.6);'
43    + 'min-width:160px;overflow:hidden}'
44    + '.wwpin-menu button{display:block;width:100%;text-align:left;background:none;border:0;'
45    + 'color:#e8e8e8;padding:9px 12px;font-size:13px;cursor:pointer}'
46    + '.wwpin-menu button:hover{background:#22252c}';
47  var st = document.createElement('style');
48  st.textContent = css;
49  document.head.appendChild(st);
50
51  function readPin() {
52    return fetch('/wavedeck_pin.php?stream_id=' + getSID(), { credentials: 'same-origin' })
53      .then(function (r) { return r.json(); });
54  }
55  function writePin(params) {
56    params.stream_id = getSID();
57    params.csrf = CSRF || '';
58    return fetch('/wavedeck_pin.php', {
59      method: 'POST', credentials: 'same-origin',
60      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
61      body: new URLSearchParams(params)
62    }).then(function (r) { return r.json(); });
63  }
64  function findBox() {
65    var r = document.querySelector(ROW);
66    if (r && r.parentElement) { return r.parentElement; }
67    return document.querySelector('#wch_msgs, .live-now-hero-chat-messages');
68  }
69  function ensureBanner() {
70    var b = findBox();
71    if (!b || !b.parentElement) { return null; }
72    if (banner && banner.parentElement) { return banner; }
73    banner = document.createElement('div');
74    banner.className = 'wwpin-bar';
75    banner.hidden = true;
76    banner.innerHTML = '<span class="wwpin-ico">&#128204;</span>'
77                     + '<span class="wwpin-body"><span class="wwpin-who"></span>'
78                     + '<span class="wwpin-txt"></span></span>';
79    b.parentElement.insertBefore(banner, b);
80    return banner;
81  }
82  function closeMenus() {
83    var m = document.querySelectorAll('.wwpin-menu');
84    for (var i = 0; i < m.length; i++) { m[i].remove(); }
85  }
86  function afterWrite(res) {
87    closeMenus();
88    if (!res || !res.ok) { alert('Pin failed: ' + ((res && res.error) || 'unknown error')); return; }
89    PIN = res.pin;
90    render();
91  }
92  function decorate() {
93    if (!CAN) { return; }
94    var rows = document.querySelectorAll(ROW + ':not([data-wwpin])');
95    for (var i = 0; i < rows.length; i++) {
96      var r = rows[i];
97      var id = r.getAttribute('data-msg-id');
98      if (!id || r.className.indexOf('is-sys') !== -1) { continue; }
99      r.setAttribute('data-wwpin', '1');
100      var d = document.createElement('button');
101      d.className = 'wwpin-dots';
102      d.type = 'button';
103      d.title = 'Message actions';
104      d.textContent = '\u22EE';
105      d.setAttribute('data-cid', id);
106      r.appendChild(d);
107    }
108  }
109  /* Cap the banner at 176px, but never more than 45% of the chat column, so a
110     long pin cannot swallow the message list. Measured rather than a CSS %:
111     the watch page's .live-now-hero-chat is sized by max-height, so its height
112     is indefinite and a percentage max-height there would compute to none. */
113  function fitBanner(el) {
114    var p = el.parentElement;
115    var h = p ? p.clientHeight : 0;
116    el.style.maxHeight = (h > 0 ? Math.min(176, Math.round(h * 0.45)) : 176) + 'px';
117  }
118  function render() {
119    var el = ensureBanner();
120    if (el) {
121      if (!PIN) {
122        el.hidden = true;
123      } else {
124        el.querySelector('.wwpin-who').textContent = PIN.author_name + ':';
125        el.querySelector('.wwpin-txt').textContent = ' ' + PIN.text;
126        var x = el.querySelector('.wwpin-x');
127        if (CAN && !x) {
128          x = document.createElement('button');
129          x.className = 'wwpin-x';
130          x.type = 'button';
131          x.title = 'Unpin';
132          x.innerHTML = '&times;';
133          x.addEventListener('click', function () { writePin({ action: 'clear' }).then(afterWrite); });
134          el.appendChild(x);
135        } else if (!CAN && x) { x.remove(); }
136        el.hidden = false;
137        fitBanner(el);
138      }
139    }
140    decorate();
141  }
142  document.addEventListener('click', function (ev) {
143    var t = ev.target;
144    var isDots = t && t.classList && t.classList.contains('wwpin-dots');
145    if (!isDots) { closeMenus(); return; }
146    ev.preventDefault();
147    ev.stopPropagation();
148    var open = t.parentElement.querySelector('.wwpin-menu');
149    closeMenus();
150    if (open) { return; }
151    var cid = t.getAttribute('data-cid');
152    var pinned = PIN && String(PIN.comment_id) === String(cid);
153    var menu = document.createElement('div');
154    menu.className = 'wwpin-menu';
155    var btn = document.createElement('button');
156    btn.type = 'button';
157    btn.textContent = pinned ? 'Unpin this message' : 'Pin this message';
158    btn.addEventListener('click', function () {
159      if (pinned) { writePin({ action: 'clear' }).then(afterWrite); }
160      else { writePin({ action: 'set', comment_id: cid }).then(afterWrite); }
161    });
162    menu.appendChild(btn);
163    var del = document.createElement('button');
164    del.type = 'button';
165    del.style.color = '#ff6b6b';
166    del.textContent = 'Delete message';
167    del.addEventListener('click', function () {
168      closeMenus();
169      if (!confirm('Permanently delete this message?\n\nIt disappears for everyone - web chat, popout, '
170                 + 'OBS overlay and the apps. This cannot be undone.')) { return; }
171      writePin({ action: 'delete_msg', comment_id: cid }).then(function (res) {
172        if (!res || !res.ok) { alert('Delete failed: ' + ((res && res.error) || 'unknown error')); return; }
173        var gone = document.querySelector(ROW + '[data-msg-id="' + cid + '"]');
174        if (gone) { gone.remove(); }
175        PIN = res.pin;
176        render();
177      });
178    });
179    menu.appendChild(del);
180    t.parentElement.appendChild(menu);
181  });
182  function refresh() {
183    if (!getSID()) { return Promise.resolve(); }
184    return readPin().then(function (d) {
185      if (!d || !d.ok) { return; }
186      CAN = !!d.can_pin;
187      if (d.csrf) { CSRF = d.csrf; }
188      if (window.WWChatLinks) { WWChatLinks.setState(d); }
189      PIN = d.pin;
190      render();
191    }).catch(function () {});
192  }
193  window.addEventListener('resize', function () {
194    if (banner && !banner.hidden) { fitBanner(banner); }
195  });
196  refresh();
197  /* WW-PUSH 26 Sep 2026: the server says when the pin changes (ww:push, topic 'pin'); the timer is
198     only a safety net for a dropped socket. Was 10 s, before that 5 s. */
199  setInterval(refresh, 60000);
200  window.addEventListener('ww:push', function (ev) {
201    var d = (ev && ev.detail) || {};
202    if (d.topic !== 'pin') { return; }
203    var sid = parseInt(d.streamId, 10) || 0;
204    if (sid && getSID() && sid !== getSID()) { return; }
205    refresh();
206  });
207  setTimeout(function () {
208    var b = findBox();
209    if (b && window.MutationObserver) {
210      new MutationObserver(decorate).observe(b, { childList: true });
211    }
212  }, 1500);
213})();

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.