openvibers/openvibe.chat Commits
No text arrows: drawn glyphs (Design rule 169) (#565)
Changes
@@@ -23,6 +23,7 @@.fv-prompt, .fv-answer, .fv-step, .fv-count, .fv-more, .fv-working { display: flex; gap: 7px; align-items: baseline; margin: 3px 0; min-width: 0; }.fv-prompt { font-weight: 600; }.fv-ic { flex: none; width: 1.1em; text-align: center; color: var(--fv-muted); }++.fv-ic svg { width: 14px; height: 14px; vertical-align: -2px; }.fv-prompt .fv-ic { color: var(--fv-accent); }.fv-answer .fv-ic { color: var(--fv-ok); }.fv-txt { overflow-wrap: anywhere; }@@@ -5,7 +5,8 @@ones fold to their prompt. Everything is set with textContent. In a frame it tells the parent its height. */(function () {'use strict';-- var ICON = { prompt: '›', response: '✓', thinking: '…', 'tool.call': '⚙', 'file.change': '✎', 'file.save': '✎', 'session.status': '•' };++ var ICON = { response: '✓', thinking: '…', 'tool.call': '⚙', 'file.change': '✎', 'file.save': '✎', 'session.status': '•' };++ var PROMPT_ICON = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg>';var LIVE_MS = 3 * 60 * 1000;function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }function store(fn) { try { return fn(window.localStorage); } catch (e) { return null; } }@@@ -75,7 +76,7 @@turns.forEach(function (turn, i) {var last = i === turns.length - 1;var li = el('li', 'fv-turn' + (last ? ' fv-cur' : ''));-- if (turn.prompt) { var p = el('p', 'fv-prompt'); p.appendChild(el('span', 'fv-ic', ICON.prompt)); p.appendChild(el('span', 'fv-txt', turn.prompt.summary)); li.appendChild(p); }++ if (turn.prompt) { var p = el('p', 'fv-prompt'); var pi = el('span', 'fv-ic'); pi.innerHTML = PROMPT_ICON; p.appendChild(pi); p.appendChild(el('span', 'fv-txt', turn.prompt.summary)); li.appendChild(p); }if (last || !turn.prompt) {var steps = el('ol', 'fv-steps');var shown = turn.steps.slice(-6);@@@ -36,6 +36,7 @@ main[data-party] [hidden] { display: none !important; }.pt-title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; text-wrap: balance; }.pt-from { flex: none; color: var(--pt-muted); font-size: 13.5px; text-decoration: none; }.pt-from:hover, .pt-from:focus-visible { color: var(--pt-text); text-decoration: underline; }++.pt-from .pt-open { margin-left: 5px; vertical-align: -2px; }[data-party-invite] { flex: none; }.pt-invite { min-height: 40px; padding: 0 18px; border: 0; border-radius: 999px; background: var(--pt-accent); color: var(--pt-on-accent); font: inherit; font-size: 14px; font-weight: 750; cursor: pointer; }.pt-invite:hover { filter: brightness(1.08); }@@@ -155,7 +155,8 @@if (shellHead) head.classList.add('pt-head');else head.appendChild(el('h1', 'pt-title', data.title || 'Watch party'));if (data.source_url) {-- var from = el('a', 'pt-from', 'Open on its page ↗');++ var from = el('a', 'pt-from', 'Open on its page ');++ from.insertAdjacentHTML('beforeend', '<svg class="pt-open" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></svg>');from.href = data.source_url; from.target = '_blank'; from.rel = 'noopener';// Beside "Started by …" when the shell has that line; otherwise in the header.var by = shellHead && shellHead.querySelector('p');@@@ -3336,7 +3336,7 @@ class ChatServer {if (client.user?.id === streamOwnerId && client.streamId === streamId) {this.sendTo(ws, {type: 'system',-- message: `⏳ New IP needs approval: ${username} (${ip}). Open Dashboard → Moderation to review.`,++ message: `⏳ New IP needs approval: ${username} (${ip}). Open Dashboard, then Moderation, to review.`,ip_approval_alert: true,});break;@@@ -130,7 +130,7 @@ function broadcast({ chatServer, saved, list, log = console }) {}return n;};-- ATTEMPTS_MS.forEach((ms, i) => { const t = setTimeout(() => { const n = push(); if (n) log.log(`[Deploy notice] ${list.length} commit(s) → ${n} client(s) (pass ${i + 1})`); }, ms); if (t.unref) t.unref(); });++ ATTEMPTS_MS.forEach((ms, i) => { const t = setTimeout(() => { const n = push(); if (n) log.log(`[Deploy notice] ${list.length} commit(s) sent to ${n} client(s) (pass ${i + 1})`); }, ms); if (t.unref) t.unref(); });}/** The release a live.release.deployed envelope announces ({ head, commits }), or an 'ignored:*' outcome. No I/O. */@@@ -114,7 +114,7 @@ function startAtBoot({ config, port, fetchImpl, log = console, retryMs = BOOT_REif (stopped) return resolve(null);try {const results = await ensure({ config, endpoint, action: 'create', fetchImpl });-- for (const r of results) log.log(`[Events] subscription ${r.result}: ${r.id} (${r.topic} → ${endpoint}${r.enabled === false ? ', DISABLED' : ''})`);++ for (const r of results) log.log(`[Events] subscription ${r.result}: ${r.id} (${r.topic} to ${endpoint}${r.enabled === false ? ', DISABLED' : ''})`);return resolve(results);} catch (err) {const next = retryMs[i + 1];@@@ -7,6 +7,8 @@ const svg = (d) => `<svg class="oc-i" viewBox="0 0 24 24" aria-hidden="true" focconst ICONS = {back: svg('<path d="M15 5l-7 7 7 7"/>'),++ up: svg('<path d="M12 20V4M6 10l6-6 6 6"/>'),++ down: svg('<path d="M12 4v16M6 14l6 6 6-6"/>'),send: svg('<path d="M12 19V5M5 12l7-7 7 7"/>'),image: svg('<rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/>'),trash: svg('<path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/>'),@@@ -31,6 +31,7 @@* test/live-down.test.js renders every page and uses the core APIs with Live refusing and hanging.*/const showcase = require('openvibe-shared/showcase');++const { ICONS } = require('./icons');const shell = require('./shell');const path = require('path');const express = require('express');@@@ -671,7 +672,7 @@ ${composer}`,await page(req, res, 200, {actor, title, path: `/call/${ch.id}`, active: 'messages', robots: 'noindex, nofollow', callScript: true,page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },-- body: `<p><a href="${backLink}">← Back to the conversation</a></p>++ body: `<p><a href="${backLink}">${ICONS.back} Back to the conversation</a></p><section class="oc-call oc-call-page" aria-labelledby="oc-call-h" id="oc-call"><h1 id="oc-call-h">${esc(title)} <span class="oc-muted oc-call-count" id="oc-call-count">${ch.participantCount ? `· ${ch.participantCount} in` : ''}</span></h1><p class="oc-muted" id="oc-call-how">A voice call, peer to peer: your microphone goes only to the people in it.</p>@@@ -1511,7 +1512,7 @@ ${entries.length ? `<ul class="oc-list">${entries.map(entryView).join('')}</ul>`res.set('Cache-Control', 'no-store');await page(req, res, 200, {actor, title: `${room.name}: new hook`, path: req.path, active: 'rooms', robots: 'noindex, nofollow',-- body: `<p><a href="/r/${esc(room.slug)}/settings#oc-hooks">← Room settings</a></p><h1>Hook "${esc(made.hook.label)}" is ready</h1>++ body: `<p><a href="/r/${esc(room.slug)}/settings#oc-hooks">${ICONS.back} Room settings</a></p><h1>Hook "${esc(made.hook.label)}" is ready</h1>${notice('ok', 'Copy the secret now: it is not shown again. Anyone with the address and the secret can post in the room.')}<label class="oc-form">Address <input readonly value="${esc(address)}"></label><label class="oc-form">Secret <input readonly value="${esc(made.secret)}"></label>@@@ -525,7 +525,7 @@ ${roles.length ? `<fieldset class="oc-set oc-role-set"><legend id="oc-onboard-roconst active = people.filter((p) => p.role !== 'blocked');const banned = people.filter((p) => p.role === 'blocked');const chanRow = (c) => `<li><span>${c.kind === 'forum' ? '▤' : c.kind === 'stage' ? '◉' : c.kind === 'voice' ? VOICE_ICON : '#'} <strong>${esc(c.name)}</strong>${c.topic ? ` <span class="oc-muted">${esc(c.topic)}</span>` : ''}</span>--<form class="oc-inline oc-moves" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}"><button type="submit" name="move" value="up" class="oc-move" aria-label="Move ${esc(c.name)} up" title="Move up">↑</button><button type="submit" name="move" value="down" class="oc-move" aria-label="Move ${esc(c.name)} down" title="Move down">↓</button></form>++<form class="oc-inline oc-moves" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}"><button type="submit" name="move" value="up" class="oc-move" aria-label="Move ${esc(c.name)} up" title="Move up">${IC.up}</button><button type="submit" name="move" value="down" class="oc-move" aria-label="Move ${esc(c.name)} down" title="Move down">${IC.down}</button></form><details class="oc-chan-edit"><summary>Edit</summary><form class="oc-form" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}"><label>Name <input name="name" required maxlength="40" value="${esc(c.name)}"></label><label>Topic <input name="topic" maxlength="200" value="${esc(c.topic || '')}"></label>@@@ -545,7 +545,7 @@ ${can.manage && c.kind !== 'voice' && require('../rooms/ai-members').enabled() ?you: me ? { name: me.display_name || me.username, status: 'Online', avatarUrl: me.avatar_url || '' } : {},}),sideLabel: server.name,-- main: `<p><a href="/s/${esc(server.slug)}">← ${esc(server.name)}</a></p><h1>Server settings</h1>${saved}${errorOf(req)}++ main: `<p><a href="/s/${esc(server.slug)}">${IC.back} ${esc(server.name)}</a></p><h1>Server settings</h1>${saved}${errorOf(req)}${can.manage ? `<h2>Overview</h2><form class="oc-form" method="post" action="/s/${esc(server.slug)}/settings"><label>Name <input name="name" required minlength="2" maxlength="40" value="${esc(server.name)}"></label><label>Description <input name="description" maxlength="300" value="${esc(server.description || '')}"></label>
@@ -23,6 +23,7 @@
@@ -23,6 +23,7 @@
.fv-prompt, .fv-answer, .fv-step, .fv-count, .fv-more, .fv-working { display: flex; gap: 7px; align-items: baseline; margin: 3px 0; min-width: 0; }
.fv-prompt, .fv-answer, .fv-step, .fv-count, .fv-more, .fv-working { display: flex; gap: 7px; align-items: baseline; margin: 3px 0; min-width: 0; }
.fv-prompt { font-weight: 600; }
.fv-prompt { font-weight: 600; }
.fv-ic { flex: none; width: 1.1em; text-align: center; color: var(--fv-muted); }
.fv-ic { flex: none; width: 1.1em; text-align: center; color: var(--fv-muted); }
+.fv-ic svg { width: 14px; height: 14px; vertical-align: -2px; }
.fv-prompt .fv-ic { color: var(--fv-accent); }
.fv-prompt .fv-ic { color: var(--fv-accent); }
.fv-answer .fv-ic { color: var(--fv-ok); }
.fv-answer .fv-ic { color: var(--fv-ok); }
.fv-txt { overflow-wrap: anywhere; }
.fv-txt { overflow-wrap: anywhere; }
@@ -5,7 +5,8 @@
@@ -5,7 +5,8 @@
ones fold to their prompt. Everything is set with textContent. In a frame it tells the parent its height. */
ones fold to their prompt. Everything is set with textContent. In a frame it tells the parent its height. */
(function () {
(function () {
'use strict';
'use strict';
- var ICON = { prompt: '›', response: '✓', thinking: '…', 'tool.call': '⚙', 'file.change': '✎', 'file.save': '✎', 'session.status': '•' };
+ var ICON = { response: '✓', thinking: '…', 'tool.call': '⚙', 'file.change': '✎', 'file.save': '✎', 'session.status': '•' };
+ var PROMPT_ICON = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg>';
var LIVE_MS = 3 * 60 * 1000;
var LIVE_MS = 3 * 60 * 1000;
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }
function store(fn) { try { return fn(window.localStorage); } catch (e) { return null; } }
function store(fn) { try { return fn(window.localStorage); } catch (e) { return null; } }
@@ -75,7 +76,7 @@
@@ -75,7 +76,7 @@
turns.forEach(function (turn, i) {
turns.forEach(function (turn, i) {
var last = i === turns.length - 1;
var last = i === turns.length - 1;
var li = el('li', 'fv-turn' + (last ? ' fv-cur' : ''));
var li = el('li', 'fv-turn' + (last ? ' fv-cur' : ''));
- if (turn.prompt) { var p = el('p', 'fv-prompt'); p.appendChild(el('span', 'fv-ic', ICON.prompt)); p.appendChild(el('span', 'fv-txt', turn.prompt.summary)); li.appendChild(p); }
+ if (turn.prompt) { var p = el('p', 'fv-prompt'); var pi = el('span', 'fv-ic'); pi.innerHTML = PROMPT_ICON; p.appendChild(pi); p.appendChild(el('span', 'fv-txt', turn.prompt.summary)); li.appendChild(p); }
if (last || !turn.prompt) {
if (last || !turn.prompt) {
var steps = el('ol', 'fv-steps');
var steps = el('ol', 'fv-steps');
var shown = turn.steps.slice(-6);
var shown = turn.steps.slice(-6);
@@ -36,6 +36,7 @@ main[data-party] [hidden] { display: none !important; }
@@ -36,6 +36,7 @@ main[data-party] [hidden] { display: none !important; }
.pt-title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; text-wrap: balance; }
.pt-title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; text-wrap: balance; }
.pt-from { flex: none; color: var(--pt-muted); font-size: 13.5px; text-decoration: none; }
.pt-from { flex: none; color: var(--pt-muted); font-size: 13.5px; text-decoration: none; }
.pt-from:hover, .pt-from:focus-visible { color: var(--pt-text); text-decoration: underline; }
.pt-from:hover, .pt-from:focus-visible { color: var(--pt-text); text-decoration: underline; }
+.pt-from .pt-open { margin-left: 5px; vertical-align: -2px; }
[data-party-invite] { flex: none; }
[data-party-invite] { flex: none; }
.pt-invite { min-height: 40px; padding: 0 18px; border: 0; border-radius: 999px; background: var(--pt-accent); color: var(--pt-on-accent); font: inherit; font-size: 14px; font-weight: 750; cursor: pointer; }
.pt-invite { min-height: 40px; padding: 0 18px; border: 0; border-radius: 999px; background: var(--pt-accent); color: var(--pt-on-accent); font: inherit; font-size: 14px; font-weight: 750; cursor: pointer; }
.pt-invite:hover { filter: brightness(1.08); }
.pt-invite:hover { filter: brightness(1.08); }
@@ -155,7 +155,8 @@
@@ -155,7 +155,8 @@
if (shellHead) head.classList.add('pt-head');
if (shellHead) head.classList.add('pt-head');
else head.appendChild(el('h1', 'pt-title', data.title || 'Watch party'));
else head.appendChild(el('h1', 'pt-title', data.title || 'Watch party'));
if (data.source_url) {
if (data.source_url) {
- var from = el('a', 'pt-from', 'Open on its page ↗');
+ var from = el('a', 'pt-from', 'Open on its page ');
+ from.insertAdjacentHTML('beforeend', '<svg class="pt-open" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M14 4h6v6M20 4l-9 9M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/></svg>');
from.href = data.source_url; from.target = '_blank'; from.rel = 'noopener';
from.href = data.source_url; from.target = '_blank'; from.rel = 'noopener';
// Beside "Started by …" when the shell has that line; otherwise in the header.
// Beside "Started by …" when the shell has that line; otherwise in the header.
var by = shellHead && shellHead.querySelector('p');
var by = shellHead && shellHead.querySelector('p');
@@ -3336,7 +3336,7 @@ class ChatServer {
@@ -3336,7 +3336,7 @@ class ChatServer {
if (client.user?.id === streamOwnerId && client.streamId === streamId) {
if (client.user?.id === streamOwnerId && client.streamId === streamId) {
this.sendTo(ws, {
this.sendTo(ws, {
type: 'system',
type: 'system',
- message: `⏳ New IP needs approval: ${username} (${ip}). Open Dashboard → Moderation to review.`,
+ message: `⏳ New IP needs approval: ${username} (${ip}). Open Dashboard, then Moderation, to review.`,
ip_approval_alert: true,
ip_approval_alert: true,
});
});
break;
break;
@@ -130,7 +130,7 @@ function broadcast({ chatServer, saved, list, log = console }) {
@@ -130,7 +130,7 @@ function broadcast({ chatServer, saved, list, log = console }) {
}
}
return n;
return n;
};
};
- ATTEMPTS_MS.forEach((ms, i) => { const t = setTimeout(() => { const n = push(); if (n) log.log(`[Deploy notice] ${list.length} commit(s) → ${n} client(s) (pass ${i + 1})`); }, ms); if (t.unref) t.unref(); });
+ ATTEMPTS_MS.forEach((ms, i) => { const t = setTimeout(() => { const n = push(); if (n) log.log(`[Deploy notice] ${list.length} commit(s) sent to ${n} client(s) (pass ${i + 1})`); }, ms); if (t.unref) t.unref(); });
}
}
/** The release a live.release.deployed envelope announces ({ head, commits }), or an 'ignored:*' outcome. No I/O. */
/** The release a live.release.deployed envelope announces ({ head, commits }), or an 'ignored:*' outcome. No I/O. */
@@ -114,7 +114,7 @@ function startAtBoot({ config, port, fetchImpl, log = console, retryMs = BOOT_RE
@@ -114,7 +114,7 @@ function startAtBoot({ config, port, fetchImpl, log = console, retryMs = BOOT_RE
if (stopped) return resolve(null);
if (stopped) return resolve(null);
try {
try {
const results = await ensure({ config, endpoint, action: 'create', fetchImpl });
const results = await ensure({ config, endpoint, action: 'create', fetchImpl });
- for (const r of results) log.log(`[Events] subscription ${r.result}: ${r.id} (${r.topic} → ${endpoint}${r.enabled === false ? ', DISABLED' : ''})`);
+ for (const r of results) log.log(`[Events] subscription ${r.result}: ${r.id} (${r.topic} to ${endpoint}${r.enabled === false ? ', DISABLED' : ''})`);
return resolve(results);
return resolve(results);
} catch (err) {
} catch (err) {
const next = retryMs[i + 1];
const next = retryMs[i + 1];
@@ -7,6 +7,8 @@ const svg = (d) => `<svg class="oc-i" viewBox="0 0 24 24" aria-hidden="true" foc
@@ -7,6 +7,8 @@ const svg = (d) => `<svg class="oc-i" viewBox="0 0 24 24" aria-hidden="true" foc
const ICONS = {
const ICONS = {
back: svg('<path d="M15 5l-7 7 7 7"/>'),
back: svg('<path d="M15 5l-7 7 7 7"/>'),
+ up: svg('<path d="M12 20V4M6 10l6-6 6 6"/>'),
+ down: svg('<path d="M12 4v16M6 14l6 6 6-6"/>'),
send: svg('<path d="M12 19V5M5 12l7-7 7 7"/>'),
send: svg('<path d="M12 19V5M5 12l7-7 7 7"/>'),
image: svg('<rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/>'),
image: svg('<rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/>'),
trash: svg('<path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/>'),
trash: svg('<path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/>'),
@@ -31,6 +31,7 @@
@@ -31,6 +31,7 @@
* test/live-down.test.js renders every page and uses the core APIs with Live refusing and hanging.
* test/live-down.test.js renders every page and uses the core APIs with Live refusing and hanging.
*/
*/
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
+const { ICONS } = require('./icons');
const shell = require('./shell');
const shell = require('./shell');
const path = require('path');
const path = require('path');
const express = require('express');
const express = require('express');
@@ -671,7 +672,7 @@ ${composer}`,
@@ -671,7 +672,7 @@ ${composer}`,
await page(req, res, 200, {
await page(req, res, 200, {
actor, title, path: `/call/${ch.id}`, active: 'messages', robots: 'noindex, nofollow', callScript: true,
actor, title, path: `/call/${ch.id}`, active: 'messages', robots: 'noindex, nofollow', callScript: true,
page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },
page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },
- body: `<p><a href="${backLink}">← Back to the conversation</a></p>
+ body: `<p><a href="${backLink}">${ICONS.back} Back to the conversation</a></p>
<section class="oc-call oc-call-page" aria-labelledby="oc-call-h" id="oc-call">
<section class="oc-call oc-call-page" aria-labelledby="oc-call-h" id="oc-call">
<h1 id="oc-call-h">${esc(title)} <span class="oc-muted oc-call-count" id="oc-call-count">${ch.participantCount ? `· ${ch.participantCount} in` : ''}</span></h1>
<h1 id="oc-call-h">${esc(title)} <span class="oc-muted oc-call-count" id="oc-call-count">${ch.participantCount ? `· ${ch.participantCount} in` : ''}</span></h1>
<p class="oc-muted" id="oc-call-how">A voice call, peer to peer: your microphone goes only to the people in it.</p>
<p class="oc-muted" id="oc-call-how">A voice call, peer to peer: your microphone goes only to the people in it.</p>
@@ -1511,7 +1512,7 @@ ${entries.length ? `<ul class="oc-list">${entries.map(entryView).join('')}</ul>`
@@ -1511,7 +1512,7 @@ ${entries.length ? `<ul class="oc-list">${entries.map(entryView).join('')}</ul>`
res.set('Cache-Control', 'no-store');
res.set('Cache-Control', 'no-store');
await page(req, res, 200, {
await page(req, res, 200, {
actor, title: `${room.name}: new hook`, path: req.path, active: 'rooms', robots: 'noindex, nofollow',
actor, title: `${room.name}: new hook`, path: req.path, active: 'rooms', robots: 'noindex, nofollow',
- body: `<p><a href="/r/${esc(room.slug)}/settings#oc-hooks">← Room settings</a></p><h1>Hook "${esc(made.hook.label)}" is ready</h1>
+ body: `<p><a href="/r/${esc(room.slug)}/settings#oc-hooks">${ICONS.back} Room settings</a></p><h1>Hook "${esc(made.hook.label)}" is ready</h1>
${notice('ok', 'Copy the secret now: it is not shown again. Anyone with the address and the secret can post in the room.')}
${notice('ok', 'Copy the secret now: it is not shown again. Anyone with the address and the secret can post in the room.')}
<label class="oc-form">Address <input readonly value="${esc(address)}"></label>
<label class="oc-form">Address <input readonly value="${esc(address)}"></label>
<label class="oc-form">Secret <input readonly value="${esc(made.secret)}"></label>
<label class="oc-form">Secret <input readonly value="${esc(made.secret)}"></label>
@@ -525,7 +525,7 @@ ${roles.length ? `<fieldset class="oc-set oc-role-set"><legend id="oc-onboard-ro
@@ -525,7 +525,7 @@ ${roles.length ? `<fieldset class="oc-set oc-role-set"><legend id="oc-onboard-ro
const active = people.filter((p) => p.role !== 'blocked');
const active = people.filter((p) => p.role !== 'blocked');
const banned = people.filter((p) => p.role === 'blocked');
const banned = people.filter((p) => p.role === 'blocked');
const chanRow = (c) => `<li><span>${c.kind === 'forum' ? '▤' : c.kind === 'stage' ? '◉' : c.kind === 'voice' ? VOICE_ICON : '#'} <strong>${esc(c.name)}</strong>${c.topic ? ` <span class="oc-muted">${esc(c.topic)}</span>` : ''}</span>
const chanRow = (c) => `<li><span>${c.kind === 'forum' ? '▤' : c.kind === 'stage' ? '◉' : c.kind === 'voice' ? VOICE_ICON : '#'} <strong>${esc(c.name)}</strong>${c.topic ? ` <span class="oc-muted">${esc(c.topic)}</span>` : ''}</span>
-<form class="oc-inline oc-moves" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}"><button type="submit" name="move" value="up" class="oc-move" aria-label="Move ${esc(c.name)} up" title="Move up">↑</button><button type="submit" name="move" value="down" class="oc-move" aria-label="Move ${esc(c.name)} down" title="Move down">↓</button></form>
+<form class="oc-inline oc-moves" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}"><button type="submit" name="move" value="up" class="oc-move" aria-label="Move ${esc(c.name)} up" title="Move up">${IC.up}</button><button type="submit" name="move" value="down" class="oc-move" aria-label="Move ${esc(c.name)} down" title="Move down">${IC.down}</button></form>
<details class="oc-chan-edit"><summary>Edit</summary><form class="oc-form" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}">
<details class="oc-chan-edit"><summary>Edit</summary><form class="oc-form" method="post" action="/s/${esc(server.slug)}/channels/${esc(c.slug)}">
<label>Name <input name="name" required maxlength="40" value="${esc(c.name)}"></label>
<label>Name <input name="name" required maxlength="40" value="${esc(c.name)}"></label>
<label>Topic <input name="topic" maxlength="200" value="${esc(c.topic || '')}"></label>
<label>Topic <input name="topic" maxlength="200" value="${esc(c.topic || '')}"></label>
@@ -545,7 +545,7 @@ ${can.manage && c.kind !== 'voice' && require('../rooms/ai-members').enabled() ?
@@ -545,7 +545,7 @@ ${can.manage && c.kind !== 'voice' && require('../rooms/ai-members').enabled() ?
you: me ? { name: me.display_name || me.username, status: 'Online', avatarUrl: me.avatar_url || '' } : {},
you: me ? { name: me.display_name || me.username, status: 'Online', avatarUrl: me.avatar_url || '' } : {},
}),
}),
sideLabel: server.name,
sideLabel: server.name,
- main: `<p><a href="/s/${esc(server.slug)}">← ${esc(server.name)}</a></p><h1>Server settings</h1>${saved}${errorOf(req)}
+ main: `<p><a href="/s/${esc(server.slug)}">${IC.back} ${esc(server.name)}</a></p><h1>Server settings</h1>${saved}${errorOf(req)}
${can.manage ? `<h2>Overview</h2><form class="oc-form" method="post" action="/s/${esc(server.slug)}/settings">
${can.manage ? `<h2>Overview</h2><form class="oc-form" method="post" action="/s/${esc(server.slug)}/settings">
<label>Name <input name="name" required minlength="2" maxlength="40" value="${esc(server.name)}"></label>
<label>Name <input name="name" required minlength="2" maxlength="40" value="${esc(server.name)}"></label>
<label>Description <input name="description" maxlength="300" value="${esc(server.description || '')}"></label>
<label>Description <input name="description" maxlength="300" value="${esc(server.description || '')}"></label>