openvibers/openvibe.meme Commits
The one Share sheet on a meme page (#88)
Changes
@@@ -15,6 +15,7 @@*/const express = require('express');const seo = require('openvibe-shared/seo');++const social = require('openvibe-shared/social');const { asyncRouter } = require('./router');const { createDiscoveryRoutes, homeJsonLd } = require('./discovery');const { sameOrigin } = require('./principal');@@@ -90,7 +91,8 @@ function createPageRoutes(ctx) {title: e.title,description,jsonLd: [jsonLd],-- body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem })),++ body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem,++ share: { url: `${base()}/m/${e.slug}`, image: social.url('meme') } })),});}r.get('/m/:slug', (req, res) => entryPage(req, res));@@@ -177,7 +179,7 @@ function createPageRoutes(ctx) {cache: c.visibility === 'public' ? PUBLIC_CACHE : null,title: template ? `A caption on ${template.name}` : 'A saved caption',description: c.lines.join(' / ').slice(0, 160),-- body: raw(views.caption({ caption: c, template, mine })),++ body: raw(views.caption({ caption: c, template, mine, share: { url: `${base()}/c/${c.id}` } })),});});@@@ -26,6 +26,11 @@ const KIND_LABEL = { joke: 'Joke', catchphrase: 'Catchphrase', bit: 'Bit', meme:const SOURCE_LABEL = { chat_message: 'Chat', vod: 'VOD', clip: 'Clip', rundown_segment: 'The Rundown', ai_paste: 'Community', stream_memory: 'Stream memory' };const statusBadge = (s) => `<span class="mm-status mm-status-${esc(s)}">${esc(STATUS_LABEL[s] || s)}</span>`;++++/** The entry's Share button, built by the shared kit (openvibe-shared 4.114.0 kit.share): the same quiet button with++ * the same share icon on every OpenVibe site. It sits right under the title's meta line, beside it when there is++ * room, so on a phone it never stands alone on a line of its own. */++const shareButton = (e, share) => kit.share({ title: e.title, url: share.url, text: e.summary || '', image: share.image || null });const kindLabel = (k) => esc(KIND_LABEL[k] || k);function clock(seconds) {@@@ -124,7 +129,7 @@ function home({ trending = [], week = null, counts = {} } = {}) {return `<div class="mm-wrap mm-home" id="mm-home">${page}</div>`;}--function entry({ e, citations = [], signedIn = false, staff = false, reported = false, problem = null } = {}) {++function entry({ e, citations = [], signedIn = false, staff = false, reported = false, problem = null, share = null } = {}) {const origin = citations.find((c) => c.role === 'origin') || citations[0] || null;const uses = citations.filter((c) => c !== origin);const examples = citations.filter((c) => c.role === 'example');@@@ -134,7 +139,7 @@ function entry({ e, citations = [], signedIn = false, staff = false, reported =<header class="mm-entry-h"><p class="mm-kicker">${kindLabel(e.kind)} · ${statusBadge(e.status)}${staff ? ' · <span class="mm-staff">staff view</span>' : ''}</p><h1>${esc(e.title)}</h1>--${e.summary ? `<p class="mm-summary">${esc(e.summary)}</p>` : ''}++${e.summary ? `<p class="mm-summary">${esc(e.summary)}${share && share.url ? ' ' + shareButton(e, share) : ''}</p>` : (share && share.url ? shareButton(e, share) : '')}<dl class="mm-facts"><div><dt>First seen</dt><dd><time datetime="${esc(e.first_seen_at)}">${esc(String(e.first_seen_at || '').slice(0, 10))}</time></dd></div><div><dt>Last seen</dt><dd><time datetime="${esc(e.last_seen_at)}">${esc(String(e.last_seen_at || '').slice(0, 10))}</time></dd></div>@@@ -213,7 +218,7 @@ ${preview ? `<figure class="mm-caption-card"><img src="${esc(preview.template.im</div>`;}--function caption({ caption: c, template, mine = false } = {}) {++function caption({ caption: c, template, mine = false, share = null } = {}) {return `<div class="mm-wrap" id="mm-caption" data-caption="${esc(c.id)}"><header class="mm-page-h"><p class="mm-kicker">A saved caption</p><h1>${esc(template ? template.name : 'Caption')}</h1><p class="mm-lede">${mine ? 'Yours. ' : ''}Text over the template image, saved as lines — never as a rendered picture.</p></header>@@@ -222,7 +227,7 @@ ${template ? `<img src="${esc(template.image_url)}" alt="">` : ''}<figcaption>${c.lines.map((l) => `<span>${esc(l)}</span>`).join('')}</figcaption></figure><dl class="mm-facts"><div><dt>Saved</dt><dd>${time(c.created_at)}</dd></div><div><dt>Visibility</dt><dd>${esc(c.visibility)}</dd></div><div><dt>Caption</dt><dd><code>${esc(c.id)}</code></dd></div></dl>--<div class="mm-actions"><a class="mm-btn" href="/make">Make another</a><a class="mm-btn mm-btn-quiet" href="/">Back to the memes</a></div>++<div class="mm-actions">${share && share.url ? kit.share({ title: template ? template.name : 'Caption', url: share.url, image: (template && template.image_url) || null }) : ''}<a class="mm-btn" href="/make">Make another</a><a class="mm-btn mm-btn-quiet" href="/">Back to the memes</a></div></div>`;}@@@ -68,6 +68,11 @@ function jsonLd(html) {assert.ok(r.text.includes('the robot is offline again'));assert.ok(r.text.includes('the robot is offline for good'));assert.ok(r.text.includes('/v/9002?t=120'));++ // The one Share sheet (openvibe-shared 4.114.0): one button, with the entry's own public address.++ const share = r.text.match(/<button[^>]*data-ov-share-sheet[^>]*>/g) || [];++ assert.strictEqual(share.length, 1, 'one Share button: ' + share.join(' | '));++ assert.ok(share[0].includes('data-url="https://openvibe.meme/m/robot-is-offline"'), share[0]);++ assert.ok(share[0].includes('data-image="/shared/social/meme.jpg'), share[0]);});await check('one entry page describes itself for search and share cards', async () => {@@@ -143,6 +148,10 @@ function jsonLd(html) {assert.ok(r.text.includes('mm-caption-card'));assert.ok(r.text.includes('the caption'));assert.ok(r.text.includes('on the page'));++ const share = r.text.match(/<button[^>]*data-ov-share-sheet[^>]*>/g) || [];++ assert.strictEqual(share.length, 1, 'one Share button: ' + share.join(' | '));++ assert.ok(share[0].includes(`data-url="https://openvibe.meme/c/${cap.id}"`), share[0]);++ assert.ok(share[0].includes('data-image="/img/template-'), 'the template is the picture');const hidden = (await t.get('/api/v1/captions', { as: alice, json: { template_id: tpl.id, lines: ['hidden one'], visibility: 'hidden' } })).json();assert.strictEqual((await t.get(`/c/${hidden.id}`)).status, 404);assert.strictEqual((await t.get(`/c/${hidden.id}`, { as: alice })).status, 200);
@@ -15,6 +15,7 @@
@@ -15,6 +15,7 @@
*/
*/
const express = require('express');
const express = require('express');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
+const social = require('openvibe-shared/social');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
const { createDiscoveryRoutes, homeJsonLd } = require('./discovery');
const { createDiscoveryRoutes, homeJsonLd } = require('./discovery');
const { sameOrigin } = require('./principal');
const { sameOrigin } = require('./principal');
@@ -90,7 +91,8 @@ function createPageRoutes(ctx) {
@@ -90,7 +91,8 @@ function createPageRoutes(ctx) {
title: e.title,
title: e.title,
description,
description,
jsonLd: [jsonLd],
jsonLd: [jsonLd],
- body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem })),
+ body: raw(views.entry({ e, citations, signedIn: Boolean(signedIn(req)), staff, reported: req.query.reported === '1', problem,
+ share: { url: `${base()}/m/${e.slug}`, image: social.url('meme') } })),
});
});
}
}
r.get('/m/:slug', (req, res) => entryPage(req, res));
r.get('/m/:slug', (req, res) => entryPage(req, res));
@@ -177,7 +179,7 @@ function createPageRoutes(ctx) {
@@ -177,7 +179,7 @@ function createPageRoutes(ctx) {
cache: c.visibility === 'public' ? PUBLIC_CACHE : null,
cache: c.visibility === 'public' ? PUBLIC_CACHE : null,
title: template ? `A caption on ${template.name}` : 'A saved caption',
title: template ? `A caption on ${template.name}` : 'A saved caption',
description: c.lines.join(' / ').slice(0, 160),
description: c.lines.join(' / ').slice(0, 160),
- body: raw(views.caption({ caption: c, template, mine })),
+ body: raw(views.caption({ caption: c, template, mine, share: { url: `${base()}/c/${c.id}` } })),
});
});
});
});
@@ -26,6 +26,11 @@ const KIND_LABEL = { joke: 'Joke', catchphrase: 'Catchphrase', bit: 'Bit', meme:
@@ -26,6 +26,11 @@ const KIND_LABEL = { joke: 'Joke', catchphrase: 'Catchphrase', bit: 'Bit', meme:
const SOURCE_LABEL = { chat_message: 'Chat', vod: 'VOD', clip: 'Clip', rundown_segment: 'The Rundown', ai_paste: 'Community', stream_memory: 'Stream memory' };
const SOURCE_LABEL = { chat_message: 'Chat', vod: 'VOD', clip: 'Clip', rundown_segment: 'The Rundown', ai_paste: 'Community', stream_memory: 'Stream memory' };
const statusBadge = (s) => `<span class="mm-status mm-status-${esc(s)}">${esc(STATUS_LABEL[s] || s)}</span>`;
const statusBadge = (s) => `<span class="mm-status mm-status-${esc(s)}">${esc(STATUS_LABEL[s] || s)}</span>`;
+
+/** The entry's Share button, built by the shared kit (openvibe-shared 4.114.0 kit.share): the same quiet button with
+ * the same share icon on every OpenVibe site. It sits right under the title's meta line, beside it when there is
+ * room, so on a phone it never stands alone on a line of its own. */
+const shareButton = (e, share) => kit.share({ title: e.title, url: share.url, text: e.summary || '', image: share.image || null });
const kindLabel = (k) => esc(KIND_LABEL[k] || k);
const kindLabel = (k) => esc(KIND_LABEL[k] || k);
function clock(seconds) {
function clock(seconds) {
@@ -124,7 +129,7 @@ function home({ trending = [], week = null, counts = {} } = {}) {
@@ -124,7 +129,7 @@ function home({ trending = [], week = null, counts = {} } = {}) {
return `<div class="mm-wrap mm-home" id="mm-home">${page}</div>`;
return `<div class="mm-wrap mm-home" id="mm-home">${page}</div>`;
}
}
-function entry({ e, citations = [], signedIn = false, staff = false, reported = false, problem = null } = {}) {
+function entry({ e, citations = [], signedIn = false, staff = false, reported = false, problem = null, share = null } = {}) {
const origin = citations.find((c) => c.role === 'origin') || citations[0] || null;
const origin = citations.find((c) => c.role === 'origin') || citations[0] || null;
const uses = citations.filter((c) => c !== origin);
const uses = citations.filter((c) => c !== origin);
const examples = citations.filter((c) => c.role === 'example');
const examples = citations.filter((c) => c.role === 'example');
@@ -134,7 +139,7 @@ function entry({ e, citations = [], signedIn = false, staff = false, reported =
@@ -134,7 +139,7 @@ function entry({ e, citations = [], signedIn = false, staff = false, reported =
<header class="mm-entry-h">
<header class="mm-entry-h">
<p class="mm-kicker">${kindLabel(e.kind)} · ${statusBadge(e.status)}${staff ? ' · <span class="mm-staff">staff view</span>' : ''}</p>
<p class="mm-kicker">${kindLabel(e.kind)} · ${statusBadge(e.status)}${staff ? ' · <span class="mm-staff">staff view</span>' : ''}</p>
<h1>${esc(e.title)}</h1>
<h1>${esc(e.title)}</h1>
-${e.summary ? `<p class="mm-summary">${esc(e.summary)}</p>` : ''}
+${e.summary ? `<p class="mm-summary">${esc(e.summary)}${share && share.url ? ' ' + shareButton(e, share) : ''}</p>` : (share && share.url ? shareButton(e, share) : '')}
<dl class="mm-facts">
<dl class="mm-facts">
<div><dt>First seen</dt><dd><time datetime="${esc(e.first_seen_at)}">${esc(String(e.first_seen_at || '').slice(0, 10))}</time></dd></div>
<div><dt>First seen</dt><dd><time datetime="${esc(e.first_seen_at)}">${esc(String(e.first_seen_at || '').slice(0, 10))}</time></dd></div>
<div><dt>Last seen</dt><dd><time datetime="${esc(e.last_seen_at)}">${esc(String(e.last_seen_at || '').slice(0, 10))}</time></dd></div>
<div><dt>Last seen</dt><dd><time datetime="${esc(e.last_seen_at)}">${esc(String(e.last_seen_at || '').slice(0, 10))}</time></dd></div>
@@ -213,7 +218,7 @@ ${preview ? `<figure class="mm-caption-card"><img src="${esc(preview.template.im
@@ -213,7 +218,7 @@ ${preview ? `<figure class="mm-caption-card"><img src="${esc(preview.template.im
</div>`;
</div>`;
}
}
-function caption({ caption: c, template, mine = false } = {}) {
+function caption({ caption: c, template, mine = false, share = null } = {}) {
return `<div class="mm-wrap" id="mm-caption" data-caption="${esc(c.id)}">
return `<div class="mm-wrap" id="mm-caption" data-caption="${esc(c.id)}">
<header class="mm-page-h"><p class="mm-kicker">A saved caption</p><h1>${esc(template ? template.name : 'Caption')}</h1>
<header class="mm-page-h"><p class="mm-kicker">A saved caption</p><h1>${esc(template ? template.name : 'Caption')}</h1>
<p class="mm-lede">${mine ? 'Yours. ' : ''}Text over the template image, saved as lines — never as a rendered picture.</p></header>
<p class="mm-lede">${mine ? 'Yours. ' : ''}Text over the template image, saved as lines — never as a rendered picture.</p></header>
@@ -222,7 +227,7 @@ ${template ? `<img src="${esc(template.image_url)}" alt="">` : ''}
@@ -222,7 +227,7 @@ ${template ? `<img src="${esc(template.image_url)}" alt="">` : ''}
<figcaption>${c.lines.map((l) => `<span>${esc(l)}</span>`).join('')}</figcaption>
<figcaption>${c.lines.map((l) => `<span>${esc(l)}</span>`).join('')}</figcaption>
</figure>
</figure>
<dl class="mm-facts"><div><dt>Saved</dt><dd>${time(c.created_at)}</dd></div><div><dt>Visibility</dt><dd>${esc(c.visibility)}</dd></div><div><dt>Caption</dt><dd><code>${esc(c.id)}</code></dd></div></dl>
<dl class="mm-facts"><div><dt>Saved</dt><dd>${time(c.created_at)}</dd></div><div><dt>Visibility</dt><dd>${esc(c.visibility)}</dd></div><div><dt>Caption</dt><dd><code>${esc(c.id)}</code></dd></div></dl>
-<div class="mm-actions"><a class="mm-btn" href="/make">Make another</a><a class="mm-btn mm-btn-quiet" href="/">Back to the memes</a></div>
+<div class="mm-actions">${share && share.url ? kit.share({ title: template ? template.name : 'Caption', url: share.url, image: (template && template.image_url) || null }) : ''}<a class="mm-btn" href="/make">Make another</a><a class="mm-btn mm-btn-quiet" href="/">Back to the memes</a></div>
</div>`;
</div>`;
}
}
@@ -68,6 +68,11 @@ function jsonLd(html) {
@@ -68,6 +68,11 @@ function jsonLd(html) {
assert.ok(r.text.includes('the robot is offline again'));
assert.ok(r.text.includes('the robot is offline again'));
assert.ok(r.text.includes('the robot is offline for good'));
assert.ok(r.text.includes('the robot is offline for good'));
assert.ok(r.text.includes('/v/9002?t=120'));
assert.ok(r.text.includes('/v/9002?t=120'));
+ // The one Share sheet (openvibe-shared 4.114.0): one button, with the entry's own public address.
+ const share = r.text.match(/<button[^>]*data-ov-share-sheet[^>]*>/g) || [];
+ assert.strictEqual(share.length, 1, 'one Share button: ' + share.join(' | '));
+ assert.ok(share[0].includes('data-url="https://openvibe.meme/m/robot-is-offline"'), share[0]);
+ assert.ok(share[0].includes('data-image="/shared/social/meme.jpg'), share[0]);
});
});
await check('one entry page describes itself for search and share cards', async () => {
await check('one entry page describes itself for search and share cards', async () => {
@@ -143,6 +148,10 @@ function jsonLd(html) {
@@ -143,6 +148,10 @@ function jsonLd(html) {
assert.ok(r.text.includes('mm-caption-card'));
assert.ok(r.text.includes('mm-caption-card'));
assert.ok(r.text.includes('the caption'));
assert.ok(r.text.includes('the caption'));
assert.ok(r.text.includes('on the page'));
assert.ok(r.text.includes('on the page'));
+ const share = r.text.match(/<button[^>]*data-ov-share-sheet[^>]*>/g) || [];
+ assert.strictEqual(share.length, 1, 'one Share button: ' + share.join(' | '));
+ assert.ok(share[0].includes(`data-url="https://openvibe.meme/c/${cap.id}"`), share[0]);
+ assert.ok(share[0].includes('data-image="/img/template-'), 'the template is the picture');
const hidden = (await t.get('/api/v1/captions', { as: alice, json: { template_id: tpl.id, lines: ['hidden one'], visibility: 'hidden' } })).json();
const hidden = (await t.get('/api/v1/captions', { as: alice, json: { template_id: tpl.id, lines: ['hidden one'], visibility: 'hidden' } })).json();
assert.strictEqual((await t.get(`/c/${hidden.id}`)).status, 404);
assert.strictEqual((await t.get(`/c/${hidden.id}`)).status, 404);
assert.strictEqual((await t.get(`/c/${hidden.id}`, { as: alice })).status, 200);
assert.strictEqual((await t.get(`/c/${hidden.id}`, { as: alice })).status, 200);