openvibers/openvibe.space Commits

Share: one Share sheet on a space's page (#208)

OP OpenVibers committed 7 days ago · e6000ca901 · 3 files, +10 −3

* Share: the one Share sheet on a space's page

Put the network's Share button (data-ov-share-sheet) beside Open and Edit
on /spaces/:slug, sharing the published space's address (or the page's
own address for a draft) with its name and description.

* Share: the shared kit button, one look everywhere

Render the one Share button with openvibe-shared 4.114.0's kit.share
(or its exact markup where the page is built in the browser), so every
site shows the same quiet button with the share icon. Remove the
site-specific Share styles.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a4bafa2..050d1e8 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -2,6 +2,7 @@
## Unreleased
++- **Share:** a space's page carries the network's one Share button (`data-ov-share-sheet`) in its actions; it shares the published space's own address (or the page's address for a draft), with its name and description. `test/spaces.test.js`.
- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.
- Usage readings (plan T5 step 14, Space slice): `server/billing.js` turns the bytes the local object store keeps (`space_blobs`, each blob once per project) into one `platform.usage-sample@1` `gb-month` reading per production project and owner per closed UTC day, queued in `billing_readings` (migration `0006_billing_readings.sql`) and relayed to OpenVibe.Billing's `billing.usage.record`. Off unless `SPACE_BILLING_INTERVAL_MS` > 0; with `SPACE_OBJECT_STORE=media` Space emits none, because Media bills the stored bytes.
- The forum moved back to OpenVibe.Community (owner decision, 2026-10-08), so Space keeps none of it. Removed: `server/forum/*`, `server/relay/*`, `server/vip/*`, `server/media/*`, `server/identity/*`, `server/{chat-rooms,votes,limits,actor-limits,events,events-consumer}.js`, `server/http/v1.js`, `server/render/{board,forum,markdown,jsonld,highlight}.js`, `public/js/space.js`, the forum suites and their fixtures, `docs/discord-relay.md` and the roadmap JSON.
ddiff --git a/server/spaces/render.js b/server/spaces/render.js
iindex d855020..d748cb5 100644
---- a/server/spaces/render.js
++++ b/server/spaces/render.js
@@@ -5,6 +5,7 @@
* Plain template strings, every value escaped; the editor's behaviour is public/js/space-editor.js.
*/
const sc = require('openvibe-shared/showcase');
++const kit = require('openvibe-shared/kit');
const { renderPage, escapeHtml: esc, asset } = require('../render/layout');
const CAP_TEXT = {
@@@ -23,7 +24,7 @@ const ago = (ms) => {
function page(o) {
return renderPage({
title: o.title, description: o.description, canonicalPath: o.path, robots: o.robots || 'index,follow',
-- active: 'spaces', styles: o.showcase ? [sc.STYLESHEET] : [], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,
++ active: 'spaces', styles: [...(o.showcase ? [sc.STYLESHEET] : []), ...(o.styles || [])], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,
});
}
@@@ -135,8 +136,9 @@ function minePage({ spaces }) {
function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
const live = Boolean(space.site_id);
++ const share = kit.share({ title: space.name, url: live && url ? url : null, text: space.description || '' });
const actions = [
-- live ? `<a class="sp-btn" href="${esc(url)}" rel="noopener"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Open</a>` : '',
++ live ? `<a class="sp-btn" href="${esc(url)}" rel="noopener"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Open</a>` : '', share,
owner ? `<a class="sp-btn ${live ? 'sp-btn-quiet' : ''}" href="/spaces/${esc(space.slug)}/edit"><i class="fa-solid fa-pen" aria-hidden="true"></i> Edit</a>` : '',
viewer ? `<form method="post" action="/spaces/${esc(space.slug)}/fork"><input type="hidden" name="csrf" value="${esc(csrf)}"><button class="sp-btn sp-btn-quiet"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</button></form>`
: `<a class="sp-btn sp-btn-quiet" href="${signInHref(`/spaces/${space.slug}`)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Sign in to fork</a>`,
@@@ -153,7 +155,7 @@ function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
</div>`;
return page({
title: space.name, description: space.description || `${space.name}: a space on OpenVibe.Space by @${space.owner_username}.`, path: `/spaces/${space.slug}`,
-- robots: live && space.visibility === 'public' ? 'index,follow' : 'noindex,nofollow', body,
++ styles: ['kit.css'], robots: live && space.visibility === 'public' ? 'index,follow' : 'noindex,nofollow', body,
jsonLd: live ? { '@context': 'https://schema.org', '@type': 'WebApplication', name: space.name, url, description: space.description || undefined, applicationCategory: 'WebApplication', author: { '@type': 'Person', name: space.owner_username } } : undefined,
});
}
ddiff --git a/test/spaces.test.js b/test/spaces.test.js
iindex ef100ed..cd83638 100644
---- a/test/spaces.test.js
++++ b/test/spaces.test.js
@@@ -177,6 +177,10 @@ function fakeAi() {
assert.strictEqual(p.status, 200);
assert.match(p.text, new RegExp(`<iframe src="${url}"`));
assert.match(p.headers['content-security-policy'], /frame-src [^;]*https:\/\/\*\.openvibe\.website/);
++ assert.strictEqual((p.text.match(/data-ov-share-sheet/g) || []).length, 1, 'one Share button');
++ assert.ok(p.text.includes(`data-url="${url}"`), 'the published space address');
++ assert.ok(p.text.includes('data-title="Guestbook"'), 'its title');
++ assert.ok(p.text.includes('class="ovk-btn ovk-btn--quiet"') && p.text.includes('<span>Share</span>'), 'the shared kit button');
});
// A visitor signs in: the connect page, a code back to the space, traded from the space's origin.