mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-11-08 07:30:42 +00:00
Tear-off tabs: title label, clickable "open & reprint" link, VoxVera.org event ID
Each of the ten tear-off tabs now reads, top to bottom: - the flyer's TITLE, auto-sized (fitTabTitles) to be as large as fits the ~1in tab — big for short titles, shrinking instead of overflowing for long ones — so every tab is clearly labeled; - a smaller, clickable "Open & reprint this flyer" link (the <a href>, so it stays live in a printed PDF) in place of the old "Click to open this poster." / "Share and reprint." (clicking, not "scan," is what a PDF link does); - the tear-off QR; and - a "Reopen at VoxVera.org — Event ID:" line (was just "Event ID:") so people know the id is for re-finding/reprinting the flyer at the site. New tab_link / tab_event_id strings in all 14 languages (VoxVera.org stays the Latin brand; the rest localizes). e2e covers the title, the link text + poster href, and the event-id label. 240 e2e across all six engines. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
085ff81c1a
commit
053ea875b8
5 files changed
+116
-11
No files matched your search
@@ -14,7 +14,7 @@ The previous Tor/OnionShare CLI version is preserved on the `legacy-tor` branch.
|
||||
- **Edit, re-publish, and delete your flyers.** Because flyers are replaceable events, the author can edit one in place (same key + Flyer Name keeps the same URL), re-publish it as a keep-alive so relays don't age it out, or delete it. Delete publishes a replaceable **tombstone** (so VoxVera hides it even where relays ignore deletions) **and** a NIP-09 deletion request — best-effort, author-only.
|
||||
- **Open from a link or ID.** Load a flyer from an `naddr`, `nevent1`, `note1`, raw 64-char event ID, or `nostr:` URL. The lookup field tolerates copy/paste whitespace and newlines, so an event ID copied off a printed tear-off (where it wraps across two lines) still resolves.
|
||||
- **Short, scannable poster URLs.** Each poster URL is a standard NIP-19 `naddr` carrying a single relay hint, placed in the URL `#fragment` (no `?addr=` query). Shorter URLs make the tear-off QR code easier to scan. Older relay-list `naddr`s and `?addr=` URLs still resolve.
|
||||
- **Tear-off tabs.** The printable flyer has ten tear-off tabs. Each tab is a single hyperlinked call to action (the URL is the link, not printed as text) and prints the full event ID so anyone can re-find the flyer by typing it.
|
||||
- **Tear-off tabs.** The printable flyer has ten tear-off tabs. Each tab is labeled with the flyer's **title** (auto-sized to be as large as fits), a clickable **"Open & reprint this flyer"** link (live in the printed PDF), a QR code, and a **"Reopen at VoxVera.org — Event ID:"** line with the full event ID so anyone can re-find and reprint the flyer by typing it in. Localized in every language.
|
||||
- **Loading state on shared links.** Opening a poster/event link shows a localized "Loading content…" placeholder (and a "Could not load this flyer." state on failure) instead of the default flyer; the default sample content appears only on the bare site URL.
|
||||
- **14 languages with RTL support.** Flyer content and UI are localized; right-to-left scripts render correctly. Letter-spacing is applied per script so cursive (Arabic/Persian) and combining (Devanagari) scripts are not broken — see [the spec](docs/nostr-static-client-spec.md#letter-spacing-by-script).
|
||||
- **US Letter and A4.** Print size defaults to the right paper for the visitor's country (from the browser locale's region — `en-US` → Letter, `en-GB` → A4, no geolocation), with a manual override. Paper size is a local print preference and is never written into the event. See the [spec](docs/nostr-static-client-spec.md#print-paper-size).
|
||||
|
||||
@@ -103,6 +103,8 @@ Default behavior: `qr_target: "flyer_url"`.
|
||||
|
||||
When publishing, the client computes the replaceable-event `naddr` and writes the generated poster URL to `tear_off_link`. The tear-off tabs and tear-off QR code point at `tear_off_link`, so people can re-open and reprint the flyer. The main flyer QR code points at `url`, so viewers can visit the creator's intended destination.
|
||||
|
||||
Each tear-off tab is labeled with the flyer's **title** (`.tear-off-title`, auto-sized by `fitTabTitles()` to be as large as fits the ~1in tab, shrinking rather than overflowing for long titles), a clickable **"Open & reprint this flyer"** link (`tab_link`, the `<a href>` so it stays live in a printed PDF), the tear-off QR, and a **"Reopen at VoxVera.org — Event ID:"** line (`tab_event_id`, the brand stays Latin; the rest is localized) with the full event id. All strings are translated in every supported language.
|
||||
|
||||
The `naddr` is encoded with a **single relay hint** (the primary configured relay) instead of the full relay list. This keeps the poster URL short and its tear-off QR code easy to scan — a typical poster URL drops from ~233 characters (full list) to ~171 — while still pointing a fresh viewer at a relay that has the event. Viewers also fall back to `DEFAULT_RELAYS`, which is where flyers are published by default. This stays a standard NIP-19 `naddr`, so any Nostr client can resolve it, and older relay-bearing `naddr` URLs continue to decode and resolve unchanged.
|
||||
|
||||
The naddr is carried in the URL **fragment** (`#naddr1...`) rather than a `?addr=` query parameter. The fragment is shorter (less QR clutter), needs no URL-encoding, and never reaches the static host. `getUrlEventReference` reads the naddr from query params, the fragment, or anywhere in the full URL, so older `?addr=` poster URLs and printed QR codes still resolve.
|
||||
|
||||
@@ -210,6 +210,38 @@ test.describe("VoxVera static client", () => {
|
||||
expect(href || "").not.toMatch(/^javascript:/i);
|
||||
});
|
||||
|
||||
test("tear-off tabs carry the title, an open-&-reprint link, and a VoxVera.org event-id", async ({ page }) => {
|
||||
const PK = "c".repeat(64);
|
||||
const IDENT = "voxvera:tab-test";
|
||||
await stubRelays(page, [{
|
||||
id: "ab".repeat(32), kind: 30078, pubkey: PK, created_at: 3000,
|
||||
tags: [["d", IDENT], ["t", "voxvera"], ["t", "flyer"], ["language", "en"]],
|
||||
content: JSON.stringify({
|
||||
type: "voxvera_flyer", version: 1, folder_name: "tab-test", lang: "en",
|
||||
name: "N", title: "BITCOIN", subtitle: "s", headline: "h", content: "c",
|
||||
url_message: "m", url: "https://example.com/x", footer_message: "f",
|
||||
tear_off_link: "https://voxvera.org/#naddr1tab", qr_target: "flyer_url"
|
||||
})
|
||||
}]);
|
||||
await page.goto("/");
|
||||
const naddr = await page.evaluate(({ pk, ident }) =>
|
||||
window.NostrTools.nip19.naddrEncode({ identifier: ident, pubkey: pk, kind: 30078, relays: [] }),
|
||||
{ pk: PK, ident: IDENT });
|
||||
await page.goto("about:blank");
|
||||
await page.goto(`/#${naddr}`);
|
||||
const tab = page.locator(".tear-off").first();
|
||||
// Title labels the tab and is auto-sized to a concrete font size.
|
||||
await expect(tab.locator(".tear-off-title")).toHaveText("BITCOIN");
|
||||
expect(await tab.locator(".tear-off-title").evaluate((n) => n.style.fontSize)).toMatch(/\d+px/);
|
||||
// The instruction line is the clickable link to the poster URL.
|
||||
const link = tab.locator("a.tear-off-link");
|
||||
await expect(link).toHaveText("Open & reprint this flyer");
|
||||
await expect(link).toHaveAttribute("href", "https://voxvera.org/#naddr1tab");
|
||||
// Event-id label points people back to VoxVera.org.
|
||||
await expect(tab.locator(".tear-off-event-id")).toContainText("Reopen at VoxVera.org");
|
||||
await expect(tab.locator(".tear-off-event-id")).toContainText("Event ID");
|
||||
});
|
||||
|
||||
test("viewer resolves a flyer from relays that ignore the #d filter", async ({ page }) => {
|
||||
// Simulate a relay that does NOT honor the addressable "#d" tag filter (it
|
||||
// returns nothing for such a REQ) but does answer a broad author query —
|
||||
|
||||
@@ -548,6 +548,27 @@ a {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Tear-off title labels each tab. Its font-size is set by fitTabTitles() in JS
|
||||
to be as large as fits; this caps the height so a long title shrinks (and at
|
||||
worst clips) instead of pushing the link/event-id out of the 1in tab. */
|
||||
.tear-off-title {
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
text-align: center;
|
||||
letter-spacing: 0;
|
||||
max-height: 0.42in;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* The clickable action line under the title (also clickable in a printed PDF). */
|
||||
.tear-off-link {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tear-off-event-id {
|
||||
margin-top: auto;
|
||||
padding-top: 4px;
|
||||
|
||||
+60
-10
@@ -183,6 +183,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "توقيع ونشر",
|
||||
published_event: "الحدث المنشور",
|
||||
share_reprint: "شارك وأعد الطباعة.",
|
||||
tab_link: "افتح وأعد طباعة هذا الملصق",
|
||||
tab_event_id: "أعد الفتح على VoxVera.org — معرّف الحدث",
|
||||
built_with: "بني بواسطة",
|
||||
viewer: "العارض",
|
||||
client_title: "عميل VoxVera Nostr",
|
||||
@@ -271,6 +273,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Signieren und veröffentlichen",
|
||||
published_event: "Veröffentlichtes Event",
|
||||
share_reprint: "Teilen und neu drucken.",
|
||||
tab_link: "Plakat öffnen & neu drucken",
|
||||
tab_event_id: "Erneut öffnen auf VoxVera.org — Event-ID",
|
||||
built_with: "Erstellt mit",
|
||||
viewer: "Viewer",
|
||||
client_title: "VoxVera Nostr-Client",
|
||||
@@ -359,6 +363,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Sign and publish",
|
||||
published_event: "Published Event",
|
||||
share_reprint: "Share and reprint.",
|
||||
tab_link: "Open & reprint this flyer",
|
||||
tab_event_id: "Reopen at VoxVera.org — Event ID",
|
||||
built_with: "Built with",
|
||||
viewer: "Viewer",
|
||||
client_title: "VoxVera Nostr Client",
|
||||
@@ -448,6 +454,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Firmar y publicar",
|
||||
published_event: "Evento publicado",
|
||||
share_reprint: "Comparta y reimprima.",
|
||||
tab_link: "Abre y reimprime este cartel",
|
||||
tab_event_id: "Reábrelo en VoxVera.org — ID del evento",
|
||||
built_with: "Creado con",
|
||||
viewer: "Visor",
|
||||
client_title: "Cliente Nostr de VoxVera",
|
||||
@@ -536,6 +544,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "امضا و انتشار",
|
||||
published_event: "رویداد منتشرشده",
|
||||
share_reprint: "به اشتراک بگذارید و دوباره چاپ کنید.",
|
||||
tab_link: "این پوستر را باز و دوباره چاپ کنید",
|
||||
tab_event_id: "در VoxVera.org دوباره باز کنید — شناسه رویداد",
|
||||
built_with: "ساخته شده با",
|
||||
viewer: "نمایشگر",
|
||||
client_title: "کلاینت Nostr VoxVera",
|
||||
@@ -624,6 +634,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Signer et publier",
|
||||
published_event: "Evenement publie",
|
||||
share_reprint: "Partagez et reimprimez.",
|
||||
tab_link: "Ouvrir et réimprimer cette affiche",
|
||||
tab_event_id: "Rouvrir sur VoxVera.org — ID de l'événement",
|
||||
built_with: "Cree avec",
|
||||
viewer: "Visionneuse",
|
||||
client_title: "Client Nostr VoxVera",
|
||||
@@ -712,6 +724,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "חתום ופרסם",
|
||||
published_event: "אירוע שפורסם",
|
||||
share_reprint: "שתף והדפס מחדש.",
|
||||
tab_link: "פתח והדפס מחדש את הכרזה",
|
||||
tab_event_id: "פתח מחדש ב-VoxVera.org — מזהה אירוע",
|
||||
built_with: "נבנה עם",
|
||||
viewer: "מציג",
|
||||
client_title: "לקוח Nostr של VoxVera",
|
||||
@@ -800,6 +814,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "हस्ताक्षर कर प्रकाशित करें",
|
||||
published_event: "प्रकाशित इवेंट",
|
||||
share_reprint: "साझा करें और फिर से प्रिंट करें।",
|
||||
tab_link: "इस पोस्टर को खोलें और फिर से प्रिंट करें",
|
||||
tab_event_id: "VoxVera.org पर फिर से खोलें — इवेंट ID",
|
||||
built_with: "इसके साथ बनाया गया",
|
||||
viewer: "दर्शक",
|
||||
client_title: "VoxVera Nostr क्लाइंट",
|
||||
@@ -888,6 +904,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "署名して公開",
|
||||
published_event: "公開済みイベント",
|
||||
share_reprint: "共有して再印刷。",
|
||||
tab_link: "開いて再印刷",
|
||||
tab_event_id: "VoxVera.org で再度開く — イベントID",
|
||||
built_with: "作成:",
|
||||
viewer: "ビューア",
|
||||
client_title: "VoxVera Nostr クライアント",
|
||||
@@ -976,6 +994,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Assinar e publicar",
|
||||
published_event: "Evento publicado",
|
||||
share_reprint: "Compartilhe e reimprima.",
|
||||
tab_link: "Abrir e reimprimir este panfleto",
|
||||
tab_event_id: "Reabrir em VoxVera.org — ID do evento",
|
||||
built_with: "Criado com",
|
||||
viewer: "Visualizador",
|
||||
client_title: "Cliente Nostr VoxVera",
|
||||
@@ -1064,6 +1084,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Подписать и опубликовать",
|
||||
published_event: "Опубликованное событие",
|
||||
share_reprint: "Поделитесь и распечатайте снова.",
|
||||
tab_link: "Открыть и распечатать снова",
|
||||
tab_event_id: "Открыть снова на VoxVera.org — ID события",
|
||||
built_with: "Создано с помощью",
|
||||
viewer: "Просмотр",
|
||||
client_title: "Клиент VoxVera Nostr",
|
||||
@@ -1152,6 +1174,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "Saini na chapisha",
|
||||
published_event: "Tukio lililochapishwa",
|
||||
share_reprint: "Shiriki na uchapishe tena.",
|
||||
tab_link: "Fungua na uchapishe tena bango hili",
|
||||
tab_event_id: "Fungua tena kwenye VoxVera.org — Kitambulisho cha tukio",
|
||||
built_with: "Imejengwa na",
|
||||
viewer: "Kitazamaji",
|
||||
client_title: "Mteja wa VoxVera Nostr",
|
||||
@@ -1240,6 +1264,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "İmzala ve yayınla",
|
||||
published_event: "Yayınlanan olay",
|
||||
share_reprint: "Paylaşın ve yeniden yazdırın.",
|
||||
tab_link: "Bu el ilanını aç ve yeniden yazdır",
|
||||
tab_event_id: "VoxVera.org'da yeniden aç — Olay ID",
|
||||
built_with: "İle yapıldı",
|
||||
viewer: "Görüntüleyici",
|
||||
client_title: "VoxVera Nostr İstemcisi",
|
||||
@@ -1328,6 +1354,8 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
publish_event: "签名并发布",
|
||||
published_event: "已发布事件",
|
||||
share_reprint: "分享并重新打印。",
|
||||
tab_link: "打开并重新打印此传单",
|
||||
tab_event_id: "在 VoxVera.org 重新打开 — 事件 ID",
|
||||
built_with: "构建自",
|
||||
viewer: "查看器",
|
||||
client_title: "VoxVera Nostr 客户端",
|
||||
@@ -2728,26 +2756,26 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
const tearOffQrSvg = makeQrSvg(tearOff);
|
||||
const contentQrSvg = makeQrSvg(contentQr);
|
||||
const sheetClass = `${tearOff ? "container" : "container no-tear-offs"} ls-${trackingPolicy(flyerLang)}`;
|
||||
const openPosterLabel = nostrLabel("open_poster", flyerLang);
|
||||
const shareReprintLabel = nostrLabel("share_reprint", flyerLang);
|
||||
const tabLinkLabel = nostrLabel("tab_link", flyerLang);
|
||||
const builtWithLabel = nostrLabel("built_with", flyerLang);
|
||||
const eventIdLabel = nostrLabel("event_id", flyerLang);
|
||||
const tabEventIdLabel = nostrLabel("tab_event_id", flyerLang);
|
||||
const eventId = String(config.event_id || "").trim();
|
||||
const tearOffStatement = [openPosterLabel, shareReprintLabel]
|
||||
.map((part) => String(part || "").trim())
|
||||
.filter(Boolean)
|
||||
.map((part) => escapeHtml(part))
|
||||
.join("<br>");
|
||||
// The flyer's title labels each tab; it is auto-sized to fit after render.
|
||||
const tabTitle = String(config.title || "").trim();
|
||||
const tabTitleHtml = tabTitle
|
||||
? `<div class="tear-off-title" dir="${escapeHtml((flyerLocale.meta && flyerLocale.meta.direction) || "ltr")}">${escapeHtml(tabTitle)}</div>`
|
||||
: "";
|
||||
const eventIdValueHtml = eventId.length > 32
|
||||
? `${escapeHtml(eventId.slice(0, 32))}<br>${escapeHtml(eventId.slice(32))}`
|
||||
: escapeHtml(eventId);
|
||||
const eventIdHtml = eventId
|
||||
? `<div class="tear-off-event-id">${escapeHtml(eventIdLabel)}:<br><span class="tear-off-event-id-value">${eventIdValueHtml}</span></div>`
|
||||
? `<div class="tear-off-event-id">${escapeHtml(tabEventIdLabel)}:<br><span class="tear-off-event-id-value">${eventIdValueHtml}</span></div>`
|
||||
: "";
|
||||
const tearOffHtml = Array.from({ length: 10 }).map(() => `
|
||||
<div class="tear-off">
|
||||
<div class="tear-off-text">
|
||||
<a href="${escapeHtml(tearOff)}" title="${escapeHtml(tearOff)}">${tearOffStatement}</a>
|
||||
${tabTitleHtml}
|
||||
<a class="tear-off-link" href="${escapeHtml(tearOff)}" title="${escapeHtml(tearOff)}">${escapeHtml(tabLinkLabel)}</a>
|
||||
${eventIdHtml}
|
||||
</div>
|
||||
<div class="qr-code" aria-label="QR code for ${escapeHtml(tearOff)}">${tearOffQrSvg}</div>
|
||||
@@ -2777,9 +2805,31 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
fitTabTitles();
|
||||
updatePreviewScale();
|
||||
}
|
||||
|
||||
// Size the tear-off title as large as it can be while still fitting the tab
|
||||
// (so short titles are big and long ones shrink instead of overflowing). All
|
||||
// ten tabs share the same title, so measure one and apply the result to all.
|
||||
function fitTabTitles() {
|
||||
const titles = el("flyer-preview") && el("flyer-preview").querySelectorAll(".tear-off-title");
|
||||
if (!titles || !titles.length) return;
|
||||
const MAX = 22;
|
||||
const MIN = 9;
|
||||
const probe = titles[0];
|
||||
let size = MAX;
|
||||
probe.style.fontSize = `${size}px`;
|
||||
// .tear-off-title caps its height via CSS; shrink until the text fits within
|
||||
// both that height and the tab width.
|
||||
const fits = () => probe.scrollHeight <= probe.clientHeight + 1 && probe.scrollWidth <= probe.clientWidth + 1;
|
||||
while (size > MIN && !fits()) {
|
||||
size -= 1;
|
||||
probe.style.fontSize = `${size}px`;
|
||||
}
|
||||
titles.forEach((node) => { node.style.fontSize = `${size}px`; });
|
||||
}
|
||||
|
||||
function updatePreviewScale() {
|
||||
const preview = el("flyer-preview");
|
||||
const sheet = preview && preview.querySelector(".container");
|
||||
|
||||
Reference in new issue
Block a user