openvibers/openvibe.live Commits

Live passes the browser check: paste screenshots from openvibe.community load (img-src), and the chat resize grip is a full ARIA separator (orientation and value) (#45)

OP OpenVibers committed 8 Oct 2026 · 10c319d09a · 2 files, +23 −4

Changes

ddiff --git a/public/js/channel-chat-resize.js b/public/js/channel-chat-resize.js
iindex 6dc423a9..07fbd222 100644
---- a/public/js/channel-chat-resize.js
++++ b/public/js/channel-chat-resize.js
@@@ -35,6 +35,20 @@
function reset(mode) { write(MODES[mode].key, 0); setSize(mode, 0); settle(); }
++ // A focusable separator is a value control (WAI-ARIA): it says its orientation and the size it sets, in pixels,
++ // kept current through every drag, key, reset and layout change.
++ function sync(g, sidebar) {
++ if (!g || !sidebar) return;
++ const mode = modeOf(); const m = MODES[mode]; const r = sidebar.getBoundingClientRect();
++ const now = Math.round(m.axis === 'x' ? r.width : r.height);
++ g.setAttribute('aria-orientation', m.axis === 'x' ? 'vertical' : 'horizontal');
++ g.setAttribute('aria-valuemin', String(m.min));
++ g.setAttribute('aria-valuemax', String(Math.round(Math.max(m.min, m.max()))));
++ g.setAttribute('aria-valuenow', String(clamp(now, m.min, Math.max(m.min, m.max()))));
++ g.setAttribute('aria-valuetext', `Chat ${now} pixels ${m.axis === 'x' ? 'wide' : 'tall'}`);
++ }
++ function syncAll() { document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach((s) => sync(s._ovGrip, s)); }
++
function bind(sidebar) {
if (!sidebar || sidebar._ovGrip) return;
const g = document.createElement('div');
@@@ -45,6 +59,7 @@
g.title = 'Drag to resize the chat · double-click for the default size';
sidebar.prepend(g);
sidebar._ovGrip = g;
++ sync(g, sidebar);
let drag = null, lastTap = 0;
g.addEventListener('pointerdown', (e) => {
@@@ -64,6 +79,7 @@
// The sheet may go below its minimum while dragging: letting go there closes it.
drag.cur = Math.round(Math.max(drag.mode === 'sheet' ? 60 : m.min, Math.min(m.max(), drag.from + delta)));
const p = page(); if (p) p.style.setProperty(m.prop, `${drag.cur}px`);
++ sync(g, sidebar);
});
const end = (e) => {
if (!drag || (e && e.pointerId !== drag.id)) return;
@@@ -85,31 +101,34 @@
}
write(m.key, clamp(d.cur, m.min, m.max()));
settle();
++ sync(g, sidebar);
};
g.addEventListener('pointerup', end);
g.addEventListener('pointercancel', end);
g.addEventListener('lostpointercapture', end);
-- g.addEventListener('dblclick', () => reset(modeOf()));
++ g.addEventListener('dblclick', () => { reset(modeOf()); sync(g, sidebar); });
g.addEventListener('keydown', (e) => {
const mode = modeOf(); const m = MODES[mode];
-- if (e.key === 'Enter') { e.preventDefault(); reset(mode); return; }
++ if (e.key === 'Enter') { e.preventDefault(); reset(mode); sync(g, sidebar); return; }
const grow = m.axis === 'x' ? { ArrowLeft: 1, ArrowRight: -1 } : { ArrowUp: 1, ArrowDown: -1 };
if (!(e.key in grow)) return;
e.preventDefault();
const r = sidebar.getBoundingClientRect();
const v = clamp((m.axis === 'x' ? r.width : r.height) + grow[e.key] * 24, m.min, m.max());
setSize(mode, v); write(m.key, v); settle();
++ sync(g, sidebar);
});
}
function init() {
applySaved();
document.querySelectorAll('#page-channel .stream-layout > .chat-sidebar').forEach(bind);
++ syncAll();
}
init();
document.addEventListener('ov:fragment', init);
// A size saved on a wide window can be too wide for a smaller one: re-clamp when the window changes.
let t = 0;
-- window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(applySaved, 150); });
++ window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(() => { applySaved(); syncAll(); }, 150); });
window.ChannelChatResize = { init, reset: () => reset(modeOf()) };
})();
ddiff --git a/server/index.js b/server/index.js
iindex 4b275927..bf10dcf6 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -371,7 +371,7 @@ app.use(helmet({
// i.ytimg.com / i.vimeocdn.com carry media-request thumbnails. Without them
// every queued item renders as a broken image, which is what the media
// request tab was doing for every YouTube link.
-- imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com"],
++ imgSrc: ["'self'", "data:", "blob:", "image.tmdb.org", "https://openvibe.network", "https://openvibe.media", "cdn.frankerfacez.com", "cdn.betterttv.net", "cdn.7tv.app", "https://files.kick.com", "https://i.ytimg.com", "https://img.youtube.com", "https://i.vimeocdn.com", "https://openvibe.community"], // paste screenshots (OpenVibe.Community owns pastes)
// https://cloudflareinsights.com is where Cloudflare Web Analytics' beacon (script-src static.cloudflareinsights.com) reports.
connectSrc: ["'self'", "wss:", "https://openvibe.network", "https://openvibe.events", "https://openvibe.media", "https://openvibe.games", "https://cdn.jsdelivr.net", "https://esm.sh", "https://static.cloudflareinsights.com", "https://cloudflareinsights.com", whipConnectSrc],
// VODs/clips play from openvibe.media (the /api proxies 302 there), which may