openvibers/openvibe.chat Commits
Rooms and conversations: the shared composer v2 (#538)
Changes
@@@ -9,7 +9,7 @@"domain": "openvibe.chat (Chat's own site: global chat, messages, rooms, settings; the service manifest records it live since 2026-09-24) and openvibe.live (/ws/chat, /api/chat/, /api/dm/, /api/tts/, /api/sounds via an nginx include in openvibe.live's vhost)","contracts": "openvibe-contracts v0.410.0","sdk": "openvibe-sdk v0.150.0",-- "shared": "openvibe-shared v4.214.0",++ "shared": "openvibe-shared v4.215.0","capabilities": ["chat.message.send","chat.event.publish",@@@ -19,7 +19,7 @@"multer": "^2.0.2","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.410.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0","pg": "^8.23.0","ws": "^8.18.0"},@@@ -1957,9 +1957,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.214.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",-- "integrity": "sha512-HvaqQOZLLFEgwDaCkrhJFfwQfsMIlOmV05PhO796JFmC7A1NScxOqp86djT6sYsK1pARMqeo0kTua5mAdXiQFw==",++ "version": "4.215.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",++ "integrity": "sha512-2hXCSsMQieDuYtdcr4KEhBmrM0rzw10T8TT/edGSsoXXQCf4bmqMWp7iPBR3NYaJgiGATcnNFdW931AbtGhfAw==","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -31,7 +31,7 @@"multer": "^2.0.2","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.410.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0","pg": "^8.23.0","ws": "^8.18.0"},@@@ -561,7 +561,7 @@var text = input.value.trim();if (!text) { e.preventDefault(); return; }e.preventDefault();-- var btn = form.querySelector('button'); if (btn) btn.disabled = true;++ var btn = form.querySelector('[data-ovt-send]') || form.querySelector('.oc-send') || form.querySelector('button'); if (btn) btn.disabled = true;fetch(base + '/messages', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }) }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }).then(function (x) {nnew file mode 100644@@@ -0,0 +1,31 @@++/* The Chat composer on the shared one (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset chat): the room,++ conversation and global message boxes keep their form (action, socket submit, no-JS post) and become the shared++ box with its tools and Send. Add stays beside them where the place takes pictures. Without ov-composer.js the++ server's form is untouched. */++(function () {++ 'use strict';++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan, { once: true }); else scan();++ function scan() {++ Array.prototype.forEach.call(document.querySelectorAll('form[data-ovc-compose]'), upgrade);++ }++ function upgrade(form) {++ if (!form || form.__ovcComposer || !window.OpenVibeComposer || !window.OpenVibeComposer.mount) return;++ var ta = form.querySelector('textarea');++ if (!ta) return;++ var add = form.querySelector('[data-oc-attach]');++ var c = window.OpenVibeComposer.mount(document.createElement('div'), {++ preset: 'chat', name: ta.name || 'message', maxLength: Number(ta.getAttribute('maxlength')) || 4000,++ placeholder: ta.placeholder || 'Write a message', label: ta.getAttribute('aria-label') || form.getAttribute('aria-label') || 'Message',++ tools: ['emoji'],++ });++ if (!c) return;++ c.text.id = ta.id || 'oc-input';++ form.textContent = '';++ while (c.form.firstChild) form.appendChild(c.form.firstChild);++ form.classList.add('ovt-composer', 'ovc-v2');++ form.dataset.preset = 'chat';++ if (add) { var tools = form.querySelector('.ovt-c-tools'); if (tools) tools.insertBefore(add, tools.firstChild); }++ form.__ovcComposer = c;++ }++ window.OVChatComposer = { scan: scan, upgrade: upgrade };++})();@@@ -174,6 +174,7 @@ ${o.feedView ? `<link rel="stylesheet" href="${asset('feed-view.css')}">\n<scrip${o.partyHome ? `<link rel="stylesheet" href="${asset('party-home.css')}">` : ''}${o.party ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<link rel="stylesheet" href="${asset('party.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n<script src="${ovServe.url('ov-sync.js')}" defer></script>\n<script src="${asset('party-providers.js')}" defer></script>\n<script src="${asset('party-provider-sync.js')}" defer></script>\n<script src="${asset('party.js')}" defer></script>` : ''}${o.script || o.app ? `<script src="${asset('agent-run-card.js')}" defer></script>` : ''}++${String(o.body || '').includes('id="oc-compose"') ? `<script src="${ovServe.url('ov-composer.js')}" defer></script>\n<script src="${asset('composer.js')}" defer></script>` : ''}${(o.scripts || []).map((n) => `<script src="${asset(n)}" defer></script>`).join('\n')}<script src="${ovServe.url('theme-loader.js')}" defer></script><script src="${ovServe.url('navbar.js')}" defer></script>@@@ -230,7 +230,7 @@ ${youBar(o.you || {})}`;function composer(o = {}) {const questHook = o.action === '/send' ? ' data-ov-guide="chat.message"' : '';const add = o.add || '';-- return `<div class="oc-dock${add ? ' has-add' : ''}"><form class="oc-compose" method="post" action="${esc(o.action || '')}" id="oc-compose"><label for="oc-input" class="oc-sr">${esc(o.label || 'Message')}</label>++ return `<div class="oc-dock${add ? ' has-add' : ''}"><form class="oc-compose" method="post" action="${esc(o.action || '')}" id="oc-compose" data-ovc-compose data-ovc-preset="chat"><label for="oc-input" class="oc-sr">${esc(o.label || 'Message')}</label><div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden></div><div class="oc-compose-row">${add ? `<button type="button" class="oc-compose-add" data-oc-attach aria-expanded="false" aria-controls="oc-add-panel" aria-label="Add a picture" title="Add a picture">${IC.plus}</button>` : ''}<textarea id="oc-input" name="message" maxlength="${Number(o.max) || 2000}" rows="1" required placeholder="${esc(o.placeholder || 'Write a message')}"></textarea><button type="submit" class="oc-send"${questHook} aria-label="Send" title="Send">${IC.send}</button></div></div></form>${add}</div>`;}nnew file mode 100644@@@ -0,0 +1,55 @@++'use strict';++// The Chat composer on the shared one (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset chat): a room or++// conversation's form keeps its action, its Add button and its no-JS post; the words and Send become the shared++// box. Real Chrome at 390 px; skipped without Chrome.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const http = require('http');++const h = require('openvibe-shared/browser-harness');++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('composer: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }++const SHARED = process.env.OV_SHARED_DIR || path.join(__dirname, '..', 'node_modules', 'openvibe-shared');++const SITE = path.join(__dirname, '..');++const form = `<div class="oc-dock has-add"><form class="oc-compose" method="post" action="/r/test" id="oc-compose" data-ovc-compose data-ovc-preset="chat"><label for="oc-input" class="oc-sr">Message</label>++<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden></div><div class="oc-compose-row"><button type="button" class="oc-compose-add" data-oc-attach aria-label="Add a picture">+</button><textarea id="oc-input" name="message" maxlength="500" rows="1" required placeholder="Write a message"></textarea><button type="submit" class="oc-send" aria-label="Send">Send</button></div></div></form></div>`;++let posted = '';++const server = http.createServer((req, res) => {++ if (req.method === 'POST') {++ let body = '';++ req.on('data', (d) => { body += d; });++ req.on('end', () => { posted = body; res.writeHead(200, { 'content-type': 'text/html' }); res.end('<!doctype html><p id="posted">posted</p>'); });++ return;++ }++ if (req.url === '/') {++ res.writeHead(200, { 'content-type': 'text/html' });++ return res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chat</title><link rel="icon" href="data:,"></head><body style="margin:0;padding:12px;background:#0b1220;color:#e6edf7;font-family:sans-serif">${form}<script src="/ov-composer.js"></script><script src="/composer.js"></script></body></html>`);++ }++ if (req.url === '/ov-composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SHARED, 'ov-composer.js'))); }++ if (req.url === '/composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SITE, 'public/web/composer.js'))); }++ res.writeHead(404); res.end();++});++(async () => {++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));++ const base = `http://127.0.0.1:${server.address().port}`;++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chat-composer-'));++ const browser = await h.launch({ tmpDir });++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));++ const page = await h.openPage(browser, await browser.newContext(), { width: 390, height: 900 });++ try {++ await page.goto(base + '/', { maxMs: 6000 });++ for (let i = 0; i < 100 && !(await page.evaluate(`!!document.querySelector('form.ovc-v2')`)); i++) await wait(40);++ const shape = await page.evaluate(`(() => { const f = document.querySelector('#oc-compose'); return { preset: f.dataset.preset, cls: f.className, action: f.getAttribute('action'), text: !!f.querySelector('#oc-input[data-ovt-text]'), add: !!f.querySelector('.ovt-c-tools [data-oc-attach]'), send: f.querySelector('[data-ovt-send]').getAttribute('aria-label'), box: !!f.querySelector('.oc-compose-box') }; })()`);++ assert.deepEqual(shape, { preset: 'chat', cls: 'oc-compose ovt-composer ovc-v2', action: '/r/test', text: true, add: true, send: 'Send', box: false });++ assert.equal(await page.evaluate(`document.documentElement.scrollWidth`), 390, 'nothing is wider than a phone');++ await page.evaluate(`(() => { const t = document.querySelector('#oc-input'); t.value = 'Hello room'; t.dispatchEvent(new Event('input', { bubbles: true })); document.querySelector('#oc-compose [data-ovt-send]').click(); })()`);++ for (let i = 0; i < 100 && !posted; i++) await wait(40);++ assert.match(posted, /message=Hello\+room/, 'the form still posts its message');++ console.log('composer: the room box is the shared chat composer, keeps Add and its action, sends, 390 px');++ } finally {++ await page.close().catch(() => {});++ await browser.close();++ server.close();++ fs.rmSync(tmpDir, { recursive: true, force: true });++ }++})().catch((e) => { console.error(e); process.exit(1); });@@@ -86,7 +86,7 @@ t('the shell: the rail, the side column, the members column and the bottom bar',const asBob = await page(`/s/${slug}`, bob);assert.ok(/data-s="leave"/.test(asBob.text) && !/data-s="settings"/.test(asBob.text) && !/data-s="delete"/.test(asBob.text), 'a member: leave, and not a word about settings or deleting');// One composer: a box, Send on its right; no picture store in this run, so no Add.-- assert.match(r.text, /<form class="oc-compose" method="post" action="\/s\/saturday-raiders\/general" id="oc-compose">/);++ assert.match(r.text, /<form class="oc-compose" method="post" action="\/s\/saturday-raiders\/general" id="oc-compose" data-ovc-compose data-ovc-preset="chat">/);assert.match(r.text, /<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden><\/div>/);assert.match(r.text, /<textarea id="oc-input" name="message" maxlength="2000" rows="1" required placeholder="Message #general"><\/textarea><button type="submit" class="oc-send" aria-label="Send"/);});
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"domain": "openvibe.chat (Chat's own site: global chat, messages, rooms, settings; the service manifest records it live since 2026-09-24) and openvibe.live (/ws/chat, /api/chat/, /api/dm/, /api/tts/, /api/sounds via an nginx include in openvibe.live's vhost)",
"domain": "openvibe.chat (Chat's own site: global chat, messages, rooms, settings; the service manifest records it live since 2026-09-24) and openvibe.live (/ws/chat, /api/chat/, /api/dm/, /api/tts/, /api/sounds via an nginx include in openvibe.live's vhost)",
"contracts": "openvibe-contracts v0.410.0",
"contracts": "openvibe-contracts v0.410.0",
"sdk": "openvibe-sdk v0.150.0",
"sdk": "openvibe-sdk v0.150.0",
- "shared": "openvibe-shared v4.214.0",
+ "shared": "openvibe-shared v4.215.0",
"capabilities": [
"capabilities": [
"chat.message.send",
"chat.message.send",
"chat.event.publish",
"chat.event.publish",
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"multer": "^2.0.2",
"multer": "^2.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.410.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.410.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"ws": "^8.18.0"
"ws": "^8.18.0"
},
},
@@ -1957,9 +1957,9 @@
@@ -1957,9 +1957,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.214.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",
- "integrity": "sha512-HvaqQOZLLFEgwDaCkrhJFfwQfsMIlOmV05PhO796JFmC7A1NScxOqp86djT6sYsK1pARMqeo0kTua5mAdXiQFw==",
+ "version": "4.215.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",
+ "integrity": "sha512-2hXCSsMQieDuYtdcr4KEhBmrM0rzw10T8TT/edGSsoXXQCf4bmqMWp7iPBR3NYaJgiGATcnNFdW931AbtGhfAw==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -31,7 +31,7 @@
@@ -31,7 +31,7 @@
"multer": "^2.0.2",
"multer": "^2.0.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.410.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.410.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.214.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.215.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"ws": "^8.18.0"
"ws": "^8.18.0"
},
},
@@ -561,7 +561,7 @@
@@ -561,7 +561,7 @@
var text = input.value.trim();
var text = input.value.trim();
if (!text) { e.preventDefault(); return; }
if (!text) { e.preventDefault(); return; }
e.preventDefault();
e.preventDefault();
- var btn = form.querySelector('button'); if (btn) btn.disabled = true;
+ var btn = form.querySelector('[data-ovt-send]') || form.querySelector('.oc-send') || form.querySelector('button'); if (btn) btn.disabled = true;
fetch(base + '/messages', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }) })
fetch(base + '/messages', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }) })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
.then(function (x) {
.then(function (x) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+/* The Chat composer on the shared one (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset chat): the room,
+ conversation and global message boxes keep their form (action, socket submit, no-JS post) and become the shared
+ box with its tools and Send. Add stays beside them where the place takes pictures. Without ov-composer.js the
+ server's form is untouched. */
+(function () {
+ 'use strict';
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan, { once: true }); else scan();
+ function scan() {
+ Array.prototype.forEach.call(document.querySelectorAll('form[data-ovc-compose]'), upgrade);
+ }
+ function upgrade(form) {
+ if (!form || form.__ovcComposer || !window.OpenVibeComposer || !window.OpenVibeComposer.mount) return;
+ var ta = form.querySelector('textarea');
+ if (!ta) return;
+ var add = form.querySelector('[data-oc-attach]');
+ var c = window.OpenVibeComposer.mount(document.createElement('div'), {
+ preset: 'chat', name: ta.name || 'message', maxLength: Number(ta.getAttribute('maxlength')) || 4000,
+ placeholder: ta.placeholder || 'Write a message', label: ta.getAttribute('aria-label') || form.getAttribute('aria-label') || 'Message',
+ tools: ['emoji'],
+ });
+ if (!c) return;
+ c.text.id = ta.id || 'oc-input';
+ form.textContent = '';
+ while (c.form.firstChild) form.appendChild(c.form.firstChild);
+ form.classList.add('ovt-composer', 'ovc-v2');
+ form.dataset.preset = 'chat';
+ if (add) { var tools = form.querySelector('.ovt-c-tools'); if (tools) tools.insertBefore(add, tools.firstChild); }
+ form.__ovcComposer = c;
+ }
+ window.OVChatComposer = { scan: scan, upgrade: upgrade };
+})();
@@ -174,6 +174,7 @@ ${o.feedView ? `<link rel="stylesheet" href="${asset('feed-view.css')}">\n<scrip
@@ -174,6 +174,7 @@ ${o.feedView ? `<link rel="stylesheet" href="${asset('feed-view.css')}">\n<scrip
${o.partyHome ? `<link rel="stylesheet" href="${asset('party-home.css')}">` : ''}
${o.partyHome ? `<link rel="stylesheet" href="${asset('party-home.css')}">` : ''}
${o.party ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<link rel="stylesheet" href="${asset('party.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n<script src="${ovServe.url('ov-sync.js')}" defer></script>\n<script src="${asset('party-providers.js')}" defer></script>\n<script src="${asset('party-provider-sync.js')}" defer></script>\n<script src="${asset('party.js')}" defer></script>` : ''}
${o.party ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<link rel="stylesheet" href="${asset('party.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>\n<script src="${ovServe.url('ov-sync.js')}" defer></script>\n<script src="${asset('party-providers.js')}" defer></script>\n<script src="${asset('party-provider-sync.js')}" defer></script>\n<script src="${asset('party.js')}" defer></script>` : ''}
${o.script || o.app ? `<script src="${asset('agent-run-card.js')}" defer></script>` : ''}
${o.script || o.app ? `<script src="${asset('agent-run-card.js')}" defer></script>` : ''}
+${String(o.body || '').includes('id="oc-compose"') ? `<script src="${ovServe.url('ov-composer.js')}" defer></script>\n<script src="${asset('composer.js')}" defer></script>` : ''}
${(o.scripts || []).map((n) => `<script src="${asset(n)}" defer></script>`).join('\n')}
${(o.scripts || []).map((n) => `<script src="${asset(n)}" defer></script>`).join('\n')}
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
@@ -230,7 +230,7 @@ ${youBar(o.you || {})}`;
@@ -230,7 +230,7 @@ ${youBar(o.you || {})}`;
function composer(o = {}) {
function composer(o = {}) {
const questHook = o.action === '/send' ? ' data-ov-guide="chat.message"' : '';
const questHook = o.action === '/send' ? ' data-ov-guide="chat.message"' : '';
const add = o.add || '';
const add = o.add || '';
- return `<div class="oc-dock${add ? ' has-add' : ''}"><form class="oc-compose" method="post" action="${esc(o.action || '')}" id="oc-compose"><label for="oc-input" class="oc-sr">${esc(o.label || 'Message')}</label>
+ return `<div class="oc-dock${add ? ' has-add' : ''}"><form class="oc-compose" method="post" action="${esc(o.action || '')}" id="oc-compose" data-ovc-compose data-ovc-preset="chat"><label for="oc-input" class="oc-sr">${esc(o.label || 'Message')}</label>
<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden></div>
<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden></div>
<div class="oc-compose-row">${add ? `<button type="button" class="oc-compose-add" data-oc-attach aria-expanded="false" aria-controls="oc-add-panel" aria-label="Add a picture" title="Add a picture">${IC.plus}</button>` : ''}<textarea id="oc-input" name="message" maxlength="${Number(o.max) || 2000}" rows="1" required placeholder="${esc(o.placeholder || 'Write a message')}"></textarea><button type="submit" class="oc-send"${questHook} aria-label="Send" title="Send">${IC.send}</button></div></div></form>${add}</div>`;
<div class="oc-compose-row">${add ? `<button type="button" class="oc-compose-add" data-oc-attach aria-expanded="false" aria-controls="oc-add-panel" aria-label="Add a picture" title="Add a picture">${IC.plus}</button>` : ''}<textarea id="oc-input" name="message" maxlength="${Number(o.max) || 2000}" rows="1" required placeholder="${esc(o.placeholder || 'Write a message')}"></textarea><button type="submit" class="oc-send"${questHook} aria-label="Send" title="Send">${IC.send}</button></div></div></form>${add}</div>`;
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+'use strict';
+// The Chat composer on the shared one (openvibe-shared 4.215.0, OpenVibeComposer.mount, preset chat): a room or
+// conversation's form keeps its action, its Add button and its no-JS post; the words and Send become the shared
+// box. Real Chrome at 390 px; skipped without Chrome.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const http = require('http');
+const h = require('openvibe-shared/browser-harness');
+if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('composer: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }
+const SHARED = process.env.OV_SHARED_DIR || path.join(__dirname, '..', 'node_modules', 'openvibe-shared');
+const SITE = path.join(__dirname, '..');
+const form = `<div class="oc-dock has-add"><form class="oc-compose" method="post" action="/r/test" id="oc-compose" data-ovc-compose data-ovc-preset="chat"><label for="oc-input" class="oc-sr">Message</label>
+<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden></div><div class="oc-compose-row"><button type="button" class="oc-compose-add" data-oc-attach aria-label="Add a picture">+</button><textarea id="oc-input" name="message" maxlength="500" rows="1" required placeholder="Write a message"></textarea><button type="submit" class="oc-send" aria-label="Send">Send</button></div></div></form></div>`;
+let posted = '';
+const server = http.createServer((req, res) => {
+ if (req.method === 'POST') {
+ let body = '';
+ req.on('data', (d) => { body += d; });
+ req.on('end', () => { posted = body; res.writeHead(200, { 'content-type': 'text/html' }); res.end('<!doctype html><p id="posted">posted</p>'); });
+ return;
+ }
+ if (req.url === '/') {
+ res.writeHead(200, { 'content-type': 'text/html' });
+ return res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chat</title><link rel="icon" href="data:,"></head><body style="margin:0;padding:12px;background:#0b1220;color:#e6edf7;font-family:sans-serif">${form}<script src="/ov-composer.js"></script><script src="/composer.js"></script></body></html>`);
+ }
+ if (req.url === '/ov-composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SHARED, 'ov-composer.js'))); }
+ if (req.url === '/composer.js') { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(SITE, 'public/web/composer.js'))); }
+ res.writeHead(404); res.end();
+});
+(async () => {
+ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
+ const base = `http://127.0.0.1:${server.address().port}`;
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chat-composer-'));
+ const browser = await h.launch({ tmpDir });
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
+ const page = await h.openPage(browser, await browser.newContext(), { width: 390, height: 900 });
+ try {
+ await page.goto(base + '/', { maxMs: 6000 });
+ for (let i = 0; i < 100 && !(await page.evaluate(`!!document.querySelector('form.ovc-v2')`)); i++) await wait(40);
+ const shape = await page.evaluate(`(() => { const f = document.querySelector('#oc-compose'); return { preset: f.dataset.preset, cls: f.className, action: f.getAttribute('action'), text: !!f.querySelector('#oc-input[data-ovt-text]'), add: !!f.querySelector('.ovt-c-tools [data-oc-attach]'), send: f.querySelector('[data-ovt-send]').getAttribute('aria-label'), box: !!f.querySelector('.oc-compose-box') }; })()`);
+ assert.deepEqual(shape, { preset: 'chat', cls: 'oc-compose ovt-composer ovc-v2', action: '/r/test', text: true, add: true, send: 'Send', box: false });
+ assert.equal(await page.evaluate(`document.documentElement.scrollWidth`), 390, 'nothing is wider than a phone');
+ await page.evaluate(`(() => { const t = document.querySelector('#oc-input'); t.value = 'Hello room'; t.dispatchEvent(new Event('input', { bubbles: true })); document.querySelector('#oc-compose [data-ovt-send]').click(); })()`);
+ for (let i = 0; i < 100 && !posted; i++) await wait(40);
+ assert.match(posted, /message=Hello\+room/, 'the form still posts its message');
+ console.log('composer: the room box is the shared chat composer, keeps Add and its action, sends, 390 px');
+ } finally {
+ await page.close().catch(() => {});
+ await browser.close();
+ server.close();
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ }
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -86,7 +86,7 @@ t('the shell: the rail, the side column, the members column and the bottom bar',
@@ -86,7 +86,7 @@ t('the shell: the rail, the side column, the members column and the bottom bar',
const asBob = await page(`/s/${slug}`, bob);
const asBob = await page(`/s/${slug}`, bob);
assert.ok(/data-s="leave"/.test(asBob.text) && !/data-s="settings"/.test(asBob.text) && !/data-s="delete"/.test(asBob.text), 'a member: leave, and not a word about settings or deleting');
assert.ok(/data-s="leave"/.test(asBob.text) && !/data-s="settings"/.test(asBob.text) && !/data-s="delete"/.test(asBob.text), 'a member: leave, and not a word about settings or deleting');
// One composer: a box, Send on its right; no picture store in this run, so no Add.
// One composer: a box, Send on its right; no picture store in this run, so no Add.
- assert.match(r.text, /<form class="oc-compose" method="post" action="\/s\/saturday-raiders\/general" id="oc-compose">/);
+ assert.match(r.text, /<form class="oc-compose" method="post" action="\/s\/saturday-raiders\/general" id="oc-compose" data-ovc-compose data-ovc-preset="chat">/);
assert.match(r.text, /<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden><\/div>/);
assert.match(r.text, /<div class="oc-compose-box"><div class="oc-compose-tray" data-oc-tray hidden><\/div>/);
assert.match(r.text, /<textarea id="oc-input" name="message" maxlength="2000" rows="1" required placeholder="Message #general"><\/textarea><button type="submit" class="oc-send" aria-label="Send"/);
assert.match(r.text, /<textarea id="oc-input" name="message" maxlength="2000" rows="1" required placeholder="Message #general"><\/textarea><button type="submit" class="oc-send" aria-label="Send"/);
});
});