2 Commits
Author SHA1 Message Date
michilisandClaude Fable 5.1 ae7664fbe0 Stop a listing page from rewriting the next one's grid after navigation.
Switching between /mints, /fedimints and /lnurl-mints showed the right list
for a moment and then flashed back to the previous ecosystem's mints. The
client router keeps every page's script alive, and onReady re-runs each
setup on every arrival, so a visited page's setup also ran on the next page.
All three grids were marked with the same bare data-mint-grid, so the stale
setup found the new grid, fetched its own type and overwrote it.

Mark and query each grid by ecosystem (data-mint-grid="cashu" etc.), the way
the home page already scopes data-home-grid, so a stale setup finds nothing
and bails. Skip the render in hydrateMintGrid when the grid has already been
detached by the router. Document the re-run-everywhere contract on onReady,
and add a static wiring test so a bare marker cannot come back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 22:36:26 +02:00
michilisandCursor 70b35f4ccc Show a brief publishing note that fades out after a few seconds.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 22:42:00 +02:00
33 changed files with 136 additions and 62 deletions
+24 -14
View File
@@ -394,14 +394,28 @@ function escapeText(value: string): string {
let pendingPaint = 0; let pendingPaint = 0;
/** Set for one paint after publishing, to mark the new card as it goes in. */ /** Set for one paint after publishing, to mark the new card as it goes in. */
let markNewest = false; let markNewest = false;
/** /** Reviews published in this session, shown at the top until relays echo them back. */
* Reviews published in this session, shown at the top until relays echo them back.
*
* Keyed by event id, holding how many relays took it: a review that only two of
* four relays accepted says so on its card, because that is a thing the author
* may want to act on.
*/
const optimistic = new Map<string, { accepted: number; total: number }>(); const optimistic = new Map<string, { accepted: number; total: number }>();
/** Optimistic cards whose brief publishing note is still visible. */
const publishingNotes = new Set<string>();
const PUBLISHING_NOTE_MS = 4_000;
const PUBLISHING_NOTE_FADE_MS = 220;
function dismissPublishingNote(id: string): void {
publishingNotes.delete(id);
const note = document.querySelector<HTMLElement>(
`[id="review-${id.toLowerCase()}"] .propagating`,
);
if (!note) return;
note.classList.add('is-out');
window.setTimeout(() => note.remove(), PUBLISHING_NOTE_FADE_MS);
}
function markOptimistic(id: string, accepted: number, total: number): void {
optimistic.set(id, { accepted, total });
publishingNotes.add(id);
window.setTimeout(() => dismissPublishingNote(id), PUBLISHING_NOTE_MS);
}
/* ---------- rendering ---------- */ /* ---------- rendering ---------- */
@@ -476,8 +490,7 @@ function escapeText(value: string): string {
target.innerHTML = slice target.innerHTML = slice
.map((review) => .map((review) =>
reviewHtml(review, f, { reviewHtml(review, f, {
propagating: optimistic.has(review.id), propagating: publishingNotes.has(review.id),
publishedTo: optimistic.get(review.id) ?? null,
permalink, permalink,
}), }),
) )
@@ -667,10 +680,7 @@ function escapeText(value: string): string {
* that they would have seen had they written it on this page. * that they would have seen had they written it on this page.
*/ */
if (handedForward && !all.some((review) => review.id === handedForward.id)) { if (handedForward && !all.some((review) => review.id === handedForward.id)) {
optimistic.set(handedForward.id, { markOptimistic(handedForward.id, handedForward.accepted, handedForward.total);
accepted: handedForward.accepted,
total: handedForward.total,
});
all.unshift({ all.unshift({
id: handedForward.id, id: handedForward.id,
pubkey: handedForward.pubkey, pubkey: handedForward.pubkey,
@@ -810,7 +820,7 @@ function escapeText(value: string): string {
}, },
onPublished: (published) => { onPublished: (published) => {
// Optimistic insert at the top. The relays will echo it back on next load. // Optimistic insert at the top. The relays will echo it back on next load.
optimistic.set(published.id, { accepted: published.accepted, total: published.total }); markOptimistic(published.id, published.accepted, published.total);
all.unshift({ all.unshift({
id: published.id, id: published.id,
pubkey: published.pubkey, pubkey: published.pubkey,
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "دي أول مراجعة من الـnpub ده، ومفيش نشاط تاني ليه", "reviews.anonNote": "دي أول مراجعة من الـnpub ده، ومفيش نشاط تاني ليه",
"reviews.published": "نُشرت", "reviews.published": "نُشرت",
"reviews.publishedPartial": "نُشرت إلى {accepted} من {total} إعادة شحن.", "reviews.publishedPartial": "نُشرت إلى {accepted} من {total} إعادة شحن.",
"reviews.propagating": "البروغات، قد يستغرق الأمر لحظة للظهور في مكان آخر.", "reviews.propagating": "بننشر على موزّعات Nostr",
"reviews.summary.line.one": "{count} تصنيف دون تعليق: {breakdown}", "reviews.summary.line.one": "{count} تصنيف دون تعليق: {breakdown}",
"reviews.summary.line.other": "{count} تصنيف دون تعليق: {breakdown}", "reviews.summary.line.other": "{count} تصنيف دون تعليق: {breakdown}",
"reviews.summary.lineDay.one": "{count} تصنيف دون تعليق على {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} تصنيف دون تعليق على {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "أول تقييم من هذا npub، لم يتم العثور على أي نشاط آخر", "reviews.anonNote": "أول تقييم من هذا npub، لم يتم العثور على أي نشاط آخر",
"reviews.published": "تم النشر.", "reviews.published": "تم النشر.",
"reviews.publishedPartial": "تم النشر إلى {accepted} من أصل {total} مرحل.", "reviews.publishedPartial": "تم النشر إلى {accepted} من أصل {total} مرحل.",
"reviews.propagating": "أثناء النشر، قد يستغرق الأمر لحظة حتى يظهر في مكان آخر.", "reviews.propagating": "جارٍ النشر إلى مرحلات Nostr",
"reviews.summary.line.one": "{count} تقييم بدون تعليق: {breakdown}", "reviews.summary.line.one": "{count} تقييم بدون تعليق: {breakdown}",
"reviews.summary.line.other": "{count} تقييمًا بدون تعليق: {breakdown}", "reviews.summary.line.other": "{count} تقييمًا بدون تعليق: {breakdown}",
"reviews.summary.lineDay.one": "تقييم {count} بدون تعليق على {when}: {breakdown}", "reviews.summary.lineDay.one": "تقييم {count} بدون تعليق على {when}: {breakdown}",
+1 -1
View File
@@ -298,7 +298,7 @@
"reviews.anonNote": "První přezkum z tohoto npub, žádná jiná činnost nenalezena", "reviews.anonNote": "První přezkum z tohoto npub, žádná jiná činnost nenalezena",
"reviews.published": "Publikováno.", "reviews.published": "Publikováno.",
"reviews.publishedPartial": "Zveřejněno{accepted}z{total}relé.", "reviews.publishedPartial": "Zveřejněno{accepted}z{total}relé.",
"reviews.propagating": "Propagatování může chvíli trvat, než se objeví jinde.", "reviews.propagating": "Publikování na Nostr relayích",
"reviews.summary.line.one": "{count}hodnocení bez komentáře:{breakdown}", "reviews.summary.line.one": "{count}hodnocení bez komentáře:{breakdown}",
"reviews.summary.line.other": "{count}ratingy bez komentáře:{breakdown}", "reviews.summary.line.other": "{count}ratingy bez komentáře:{breakdown}",
"reviews.summary.line.few": "{count}ratingy bez komentáře:{breakdown}", "reviews.summary.line.few": "{count}ratingy bez komentáře:{breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Første anmeldelse af denne npub; der er ikke fundet andre aktiviteter", "reviews.anonNote": "Første anmeldelse af denne npub; der er ikke fundet andre aktiviteter",
"reviews.published": "Udgivet.", "reviews.published": "Udgivet.",
"reviews.publishedPartial": "Udgivet til {accepted} blandt {total}-relæerne.", "reviews.publishedPartial": "Udgivet til {accepted} blandt {total}-relæerne.",
"reviews.propagating": "Når indholdet opdateres, kan det tage et øjeblik, før det vises andre steder.", "reviews.propagating": "Udgiver til Nostr-relays",
"reviews.summary.line.one": "{count}-vurdering uden kommentar: {breakdown}", "reviews.summary.line.one": "{count}-vurdering uden kommentar: {breakdown}",
"reviews.summary.line.other": "{count}-vurderinger uden kommentar: {breakdown}", "reviews.summary.line.other": "{count}-vurderinger uden kommentar: {breakdown}",
"reviews.summary.lineDay.one": "{count}-vurdering uden kommentar til {when}: {breakdown}", "reviews.summary.lineDay.one": "{count}-vurdering uden kommentar til {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Erste Rezension von diesem Verlag, keine weiteren Aktivitäten gefunden", "reviews.anonNote": "Erste Rezension von diesem Verlag, keine weiteren Aktivitäten gefunden",
"reviews.published": "Veröffentlicht.", "reviews.published": "Veröffentlicht.",
"reviews.publishedPartial": "Veröffentlicht auf {accepted} der {total}-Relais.", "reviews.publishedPartial": "Veröffentlicht auf {accepted} der {total}-Relais.",
"reviews.propagating": "Die Übertragung kann einen Moment dauern, bis sie an anderer Stelle angezeigt wird.", "reviews.propagating": "Veröffentlichen auf Nostr-Relays",
"reviews.summary.line.one": "{count}-Bewertung ohne Kommentar: {breakdown}", "reviews.summary.line.one": "{count}-Bewertung ohne Kommentar: {breakdown}",
"reviews.summary.line.other": "{count}-Bewertungen ohne Kommentar: {breakdown}", "reviews.summary.line.other": "{count}-Bewertungen ohne Kommentar: {breakdown}",
"reviews.summary.lineDay.one": "{count}-Bewertung ohne Kommentar auf {when}: {breakdown}", "reviews.summary.lineDay.one": "{count}-Bewertung ohne Kommentar auf {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Πρώτη κριτική από αυτό το npub, δεν βρέθηκε άλλη δραστηριότητα", "reviews.anonNote": "Πρώτη κριτική από αυτό το npub, δεν βρέθηκε άλλη δραστηριότητα",
"reviews.published": "Δημοσίευση.", "reviews.published": "Δημοσίευση.",
"reviews.publishedPartial": "Δημοσίευση στο{accepted} του{total} ⁇ λεμάν.", "reviews.publishedPartial": "Δημοσίευση στο{accepted} του{total} ⁇ λεμάν.",
"reviews.propagating": "Διαδοχικά, μπορεί να πάρει μια στιγμή για να εμφανιστεί αλλού.", "reviews.propagating": "Δημοσίευση στα relays του Nostr",
"reviews.summary.line.one": "{count} αξιολόγηση χωρίς σχόλιο:{breakdown}", "reviews.summary.line.one": "{count} αξιολόγηση χωρίς σχόλιο:{breakdown}",
"reviews.summary.line.other": "{count} αξιολογήσεις χωρίς σχόλιο:{breakdown}", "reviews.summary.line.other": "{count} αξιολογήσεις χωρίς σχόλιο:{breakdown}",
"reviews.summary.lineDay.one": "{count} αξιολόγηση χωρίς σχόλιο σχετικά με{when}:{breakdown}", "reviews.summary.lineDay.one": "{count} αξιολόγηση χωρίς σχόλιο σχετικά με{when}:{breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "First review from this npub, no other activity found", "reviews.anonNote": "First review from this npub, no other activity found",
"reviews.published": "Published.", "reviews.published": "Published.",
"reviews.publishedPartial": "Published to {accepted} of {total} relays.", "reviews.publishedPartial": "Published to {accepted} of {total} relays.",
"reviews.propagating": "Propagating, it may take a moment to appear elsewhere.", "reviews.propagating": "Publishing to Nostr relays",
"reviews.summary.line.one": "{count} rating without a comment: {breakdown}", "reviews.summary.line.one": "{count} rating without a comment: {breakdown}",
"reviews.summary.line.other": "{count} ratings without a comment: {breakdown}", "reviews.summary.line.other": "{count} ratings without a comment: {breakdown}",
"reviews.summary.lineDay.one": "{count} rating without a comment on {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} rating without a comment on {when}: {breakdown}",
+1 -1
View File
@@ -286,7 +286,7 @@
"reviews.anonNote": "Primera reseña de este npub, no se ha encontrado ninguna otra actividad", "reviews.anonNote": "Primera reseña de este npub, no se ha encontrado ninguna otra actividad",
"reviews.published": "Publicada.", "reviews.published": "Publicada.",
"reviews.publishedPartial": "Publicada en {accepted} de {total} relays.", "reviews.publishedPartial": "Publicada en {accepted} de {total} relays.",
"reviews.propagating": "Propagándose, puede tardar un momento en aparecer en otros sitios.", "reviews.propagating": "Publicando en relés Nostr",
"reviews.summary.line.one": "{count} valoración sin comentario: {breakdown}", "reviews.summary.line.one": "{count} valoración sin comentario: {breakdown}",
"reviews.summary.line.other": "{count} valoraciones sin comentario: {breakdown}", "reviews.summary.line.other": "{count} valoraciones sin comentario: {breakdown}",
"reviews.summary.lineDay.one": "{count} valoración sin comentario del {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} valoración sin comentario del {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Premier avis sur ce npub, aucune autre activité trouvée", "reviews.anonNote": "Premier avis sur ce npub, aucune autre activité trouvée",
"reviews.published": "Publié.", "reviews.published": "Publié.",
"reviews.publishedPartial": "Publié sur {accepted} des relais {total}.", "reviews.publishedPartial": "Publié sur {accepted} des relais {total}.",
"reviews.propagating": "En se propageant, cela peut prendre un moment pour apparaître ailleurs.", "reviews.propagating": "Publication sur les relais Nostr",
"reviews.summary.line.one": "Note {count} sans commentaire : {breakdown}", "reviews.summary.line.one": "Note {count} sans commentaire : {breakdown}",
"reviews.summary.line.other": "Notes {count} sans commentaire : {breakdown}", "reviews.summary.line.other": "Notes {count} sans commentaire : {breakdown}",
"reviews.summary.lineDay.one": "Note {count} sans commentaire sur {when} : {breakdown}", "reviews.summary.lineDay.one": "Note {count} sans commentaire sur {when} : {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "इस npub से पहली समीक्षा, कोई अन्य गतिविधि नहीं मिली।", "reviews.anonNote": "इस npub से पहली समीक्षा, कोई अन्य गतिविधि नहीं मिली।",
"reviews.published": "प्रकाशित", "reviews.published": "प्रकाशित",
"reviews.publishedPartial": "{total} रिलेज़ में से {accepted} पर प्रकाशित।", "reviews.publishedPartial": "{total} रिलेज़ में से {accepted} पर प्रकाशित।",
"reviews.propagating": "प्रसारित करते समय, कहीं और प्रकट होने में थोड़ा समय लग सकता है।", "reviews.propagating": "Nostr रिले पर प्रकाशित किया जा रहा है",
"reviews.summary.line.one": "बिना टिप्पणी के {count} रेटिंग: {breakdown}", "reviews.summary.line.one": "बिना टिप्पणी के {count} रेटिंग: {breakdown}",
"reviews.summary.line.other": "टिप्पणी के बिना रेटिंग: {count}; रेटिंग: {breakdown}", "reviews.summary.line.other": "टिप्पणी के बिना रेटिंग: {count}; रेटिंग: {breakdown}",
"reviews.summary.lineDay.one": "{count} रेटिंग, {when} पर बिना टिप्पणी के: {breakdown}", "reviews.summary.lineDay.one": "{count} रेटिंग, {when} पर बिना टिप्पणी के: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Ulasan pertama dari npub ini, tidak ada aktivitas lain ditemukan", "reviews.anonNote": "Ulasan pertama dari npub ini, tidak ada aktivitas lain ditemukan",
"reviews.published": "Diterbitkan.", "reviews.published": "Diterbitkan.",
"reviews.publishedPartial": "Diterbitkan ke {accepted} dari {total} relay.", "reviews.publishedPartial": "Diterbitkan ke {accepted} dari {total} relay.",
"reviews.propagating": "Sedang disebarkan; mungkin perlu waktu untuk muncul di tempat lain.", "reviews.propagating": "Mempublikasikan ke relay Nostr",
"reviews.summary.line.one": "Rating {count} tanpa komentar: {breakdown}", "reviews.summary.line.one": "Rating {count} tanpa komentar: {breakdown}",
"reviews.summary.line.other": "Rating {count} tanpa komentar: {breakdown}", "reviews.summary.line.other": "Rating {count} tanpa komentar: {breakdown}",
"reviews.summary.lineDay.one": "{count} rating tanpa komentar di {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} rating tanpa komentar di {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Prima recensione da questo npub, nessun'altra attività trovata", "reviews.anonNote": "Prima recensione da questo npub, nessun'altra attività trovata",
"reviews.published": "Pubblicato.", "reviews.published": "Pubblicato.",
"reviews.publishedPartial": "Pubblicato in {accepted} di {total} relè.", "reviews.publishedPartial": "Pubblicato in {accepted} di {total} relè.",
"reviews.propagating": "Propagando, può richiedere un momento per apparire altrove.", "reviews.propagating": "Pubblicazione sui relay Nostr",
"reviews.summary.line.one": "{count} valutazione senza un commento: {breakdown}", "reviews.summary.line.one": "{count} valutazione senza un commento: {breakdown}",
"reviews.summary.line.other": "{count} valutazioni senza un commento: {breakdown}", "reviews.summary.line.other": "{count} valutazioni senza un commento: {breakdown}",
"reviews.summary.lineDay.one": "{count} valutazione senza un commento su {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} valutazione senza un commento su {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "このnpubからの最初のレビュー、他の活動が見つかりません", "reviews.anonNote": "このnpubからの最初のレビュー、他の活動が見つかりません",
"reviews.published": "掲載情報", "reviews.published": "掲載情報",
"reviews.publishedPartial": "に公開 {accepted} の {total} リレー.", "reviews.publishedPartial": "に公開 {accepted} の {total} リレー.",
"reviews.propagating": "伝播は、他の場所で出現する瞬間を取るかもしれません。", "reviews.propagating": "Nostrリレーに公開中",
"reviews.summary.line.one": "{count} コメントのない評価: {breakdown}", "reviews.summary.line.one": "{count} コメントのない評価: {breakdown}",
"reviews.summary.line.other": "{count} コメントのない評価: {breakdown}", "reviews.summary.line.other": "{count} コメントのない評価: {breakdown}",
"reviews.summary.lineDay.one": "{count} 評価なし にコメント {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} 評価なし にコメント {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Eerste review van deze npub, verder geen activiteit gevonden", "reviews.anonNote": "Eerste review van deze npub, verder geen activiteit gevonden",
"reviews.published": "Gepubliceerd.", "reviews.published": "Gepubliceerd.",
"reviews.publishedPartial": "Gepubliceerd op {accepted} van de {total} relays.", "reviews.publishedPartial": "Gepubliceerd op {accepted} van de {total} relays.",
"reviews.propagating": "Wordt verspreid, het kan even duren voordat hij elders verschijnt.", "reviews.propagating": "Publiceren naar Nostr-relays",
"reviews.summary.line.one": "{count} beoordeling zonder tekst: {breakdown}", "reviews.summary.line.one": "{count} beoordeling zonder tekst: {breakdown}",
"reviews.summary.line.other": "{count} beoordelingen zonder tekst: {breakdown}", "reviews.summary.line.other": "{count} beoordelingen zonder tekst: {breakdown}",
"reviews.summary.lineDay.one": "{count} beoordeling zonder tekst op {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} beoordeling zonder tekst op {when}: {breakdown}",
+1 -1
View File
@@ -298,7 +298,7 @@
"reviews.anonNote": "Pierwsza recenzja z tego npubu, nie znaleziono żadnej innej aktywności", "reviews.anonNote": "Pierwsza recenzja z tego npubu, nie znaleziono żadnej innej aktywności",
"reviews.published": "Opublikowano.", "reviews.published": "Opublikowano.",
"reviews.publishedPartial": "Opublikowano dla {accepted} sztafetyi {total}.", "reviews.publishedPartial": "Opublikowano dla {accepted} sztafetyi {total}.",
"reviews.propagating": "Rozmnażając, może potrzebować chwili, by pojawić się gdzie indziej.", "reviews.propagating": "Publikowanie na relayach Nostr",
"reviews.summary.line.one": "{count} ocena bez komentarza: {breakdown}", "reviews.summary.line.one": "{count} ocena bez komentarza: {breakdown}",
"reviews.summary.line.few": "{count} oceny bez komentarza: {breakdown}", "reviews.summary.line.few": "{count} oceny bez komentarza: {breakdown}",
"reviews.summary.line.many": "{count} ocen bez komentarza: {breakdown}", "reviews.summary.line.many": "{count} ocen bez komentarza: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Primeira revisão deste npub, nenhuma outra atividade encontrada", "reviews.anonNote": "Primeira revisão deste npub, nenhuma outra atividade encontrada",
"reviews.published": "Publicado.", "reviews.published": "Publicado.",
"reviews.publishedPartial": "Publicado em {accepted} dos relés {total}.", "reviews.publishedPartial": "Publicado em {accepted} dos relés {total}.",
"reviews.propagating": "Propagando, pode demorar um pouco para aparecer em outro lugar.", "reviews.propagating": "Publicando nos relays Nostr",
"reviews.summary.line.one": "Classificação {count} sem comentários: {breakdown}", "reviews.summary.line.one": "Classificação {count} sem comentários: {breakdown}",
"reviews.summary.line.other": "Avaliações {count} sem comentários: {breakdown}", "reviews.summary.line.other": "Avaliações {count} sem comentários: {breakdown}",
"reviews.summary.lineDay.one": "Classificação {count} sem comentários em {when}: {breakdown}", "reviews.summary.lineDay.one": "Classificação {count} sem comentários em {when}: {breakdown}",
+1 -1
View File
@@ -290,7 +290,7 @@
"reviews.anonNote": "Prima revizuire a acestei npub-uri, nicio altă activitate nu a fost găsită", "reviews.anonNote": "Prima revizuire a acestei npub-uri, nicio altă activitate nu a fost găsită",
"reviews.published": "Publicat.", "reviews.published": "Publicat.",
"reviews.publishedPartial": "Publicată în{accepted} din{total} relee.", "reviews.publishedPartial": "Publicată în{accepted} din{total} relee.",
"reviews.propagating": "Propaganda, poate dura un moment să apară în altă parte.", "reviews.propagating": "Se publică pe relayurile Nostr",
"reviews.summary.line.one": "{count}rating fără comentarii:{breakdown}", "reviews.summary.line.one": "{count}rating fără comentarii:{breakdown}",
"reviews.summary.line.other": "{count} ratinguri fără comentarii:{breakdown}", "reviews.summary.line.other": "{count} ratinguri fără comentarii:{breakdown}",
"reviews.summary.line.few": "{count} ratinguri fără comentarii:{breakdown}", "reviews.summary.line.few": "{count} ratinguri fără comentarii:{breakdown}",
+1 -1
View File
@@ -298,7 +298,7 @@
"reviews.anonNote": "Первый отзыв с этого npub, другой активности не найдено", "reviews.anonNote": "Первый отзыв с этого npub, другой активности не найдено",
"reviews.published": "Опубликовано.", "reviews.published": "Опубликовано.",
"reviews.publishedPartial": "Опубликовано на {accepted} из {total} relays.", "reviews.publishedPartial": "Опубликовано на {accepted} из {total} relays.",
"reviews.propagating": "Распространяется по сети, в других местах отзыв может появиться не сразу.", "reviews.propagating": "Публикация на relays Nostr",
"reviews.summary.line.one": "Рейтинг «{count}» без комментариев: {breakdown}", "reviews.summary.line.one": "Рейтинг «{count}» без комментариев: {breakdown}",
"reviews.summary.line.other": "Рейтинги «{count}» без комментариев: {breakdown}", "reviews.summary.line.other": "Рейтинги «{count}» без комментариев: {breakdown}",
"reviews.summary.line.few": "Рейтинги «{count}» без комментариев: {breakdown}", "reviews.summary.line.few": "Рейтинги «{count}» без комментариев: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Första recensionen av denna npub, inga andra aktiviteter har hittats", "reviews.anonNote": "Första recensionen av denna npub, inga andra aktiviteter har hittats",
"reviews.published": "Publicerad.", "reviews.published": "Publicerad.",
"reviews.publishedPartial": "Publicerad till reläet {accepted} i serien {total}.", "reviews.publishedPartial": "Publicerad till reläet {accepted} i serien {total}.",
"reviews.propagating": "När innehållet sprids kan det ta en stund innan det visas på andra ställen.", "reviews.propagating": "Publicerar till Nostr-relayer",
"reviews.summary.line.one": "{count}-betyg utan kommentar: {breakdown}", "reviews.summary.line.one": "{count}-betyg utan kommentar: {breakdown}",
"reviews.summary.line.other": "{count}-betyg utan kommentar: {breakdown}", "reviews.summary.line.other": "{count}-betyg utan kommentar: {breakdown}",
"reviews.summary.lineDay.one": "Betyg för {count} utan kommentar om {when}: {breakdown}", "reviews.summary.lineDay.one": "Betyg för {count} utan kommentar om {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Bu npub'dan ilk inceleme, başka bir faaliyet bulunmadı", "reviews.anonNote": "Bu npub'dan ilk inceleme, başka bir faaliyet bulunmadı",
"reviews.published": "Yayınlandı.", "reviews.published": "Yayınlandı.",
"reviews.publishedPartial": "{accepted} {total} rölesine yayımlandı.", "reviews.publishedPartial": "{accepted} {total} rölesine yayımlandı.",
"reviews.propagating": "Yayılırken başka bir yerde ortaya çıkması biraz zaman alabilir.", "reviews.propagating": "Nostr relay'lerine yayınlanıyor",
"reviews.summary.line.one": "{count} yorumsuz puan: {breakdown}", "reviews.summary.line.one": "{count} yorumsuz puan: {breakdown}",
"reviews.summary.line.other": "{count} yorumsuz puanlar: {breakdown}", "reviews.summary.line.other": "{count} yorumsuz puanlar: {breakdown}",
"reviews.summary.lineDay.one": "{when} yorumsuz {count} puanı: {breakdown}", "reviews.summary.lineDay.one": "{when} yorumsuz {count} puanı: {breakdown}",
+1 -1
View File
@@ -298,7 +298,7 @@
"reviews.anonNote": "Перший відгук від цього npub, іншої активності не знайдено", "reviews.anonNote": "Перший відгук від цього npub, іншої активності не знайдено",
"reviews.published": "Опубліковано.", "reviews.published": "Опубліковано.",
"reviews.publishedPartial": "Опубліковано на {accepted} із {total} relays.", "reviews.publishedPartial": "Опубліковано на {accepted} із {total} relays.",
"reviews.propagating": "Поширюється мережею, поява в інших місцях може тривати деякий час.", "reviews.propagating": "Публікація на relays Nostr",
"reviews.summary.line.one": "{count} рейтинг без коментаря: {breakdown}", "reviews.summary.line.one": "{count} рейтинг без коментаря: {breakdown}",
"reviews.summary.line.few": "{count} оцінки без коментарів: {breakdown}", "reviews.summary.line.few": "{count} оцінки без коментарів: {breakdown}",
"reviews.summary.line.many": "{count} оцінок без коментарів: {breakdown}", "reviews.summary.line.many": "{count} оцінок без коментарів: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "اس ناول سے پہلا جائزہ، کوئی اور سرگرمی نہیں پائی گئی", "reviews.anonNote": "اس ناول سے پہلا جائزہ، کوئی اور سرگرمی نہیں پائی گئی",
"reviews.published": "شائع ہوا۔", "reviews.published": "شائع ہوا۔",
"reviews.publishedPartial": "شائع شدہ{accepted}کا مطلب{total}ریلویز.", "reviews.publishedPartial": "شائع شدہ{accepted}کا مطلب{total}ریلویز.",
"reviews.propagating": "اِس لئے شاید آپ کسی اَور ملک میں جا کر اُس کے بارے میں بات کریں ۔", "reviews.propagating": "Nostr ریلے پر شائع ہو رہا ہے",
"reviews.summary.line.one": "{count}بغیر تبصرے کے شرحیں:{breakdown}", "reviews.summary.line.one": "{count}بغیر تبصرے کے شرحیں:{breakdown}",
"reviews.summary.line.other": "{count}بغیر تبصرے کے شرحیں:{breakdown}", "reviews.summary.line.other": "{count}بغیر تبصرے کے شرحیں:{breakdown}",
"reviews.summary.lineDay.one": "{count}بغیر تبصرے کے خواندگی{when}:{breakdown}", "reviews.summary.lineDay.one": "{count}بغیر تبصرے کے خواندگی{when}:{breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "Xem lại lần đầu từ npb này, không tìm thấy hoạt động nào khác", "reviews.anonNote": "Xem lại lần đầu từ npb này, không tìm thấy hoạt động nào khác",
"reviews.published": "Xuất bản.", "reviews.published": "Xuất bản.",
"reviews.publishedPartial": "Do {accepted} của {total} rơle.", "reviews.publishedPartial": "Do {accepted} của {total} rơle.",
"reviews.propagating": "Tuyên truyền, có thể phải mất một lúc mới xuất hiện.", "reviews.propagating": "Đang đăng lên các relay Nostr",
"reviews.summary.line.one": "{count} đánh giá mà không bình luận: {breakdown}", "reviews.summary.line.one": "{count} đánh giá mà không bình luận: {breakdown}",
"reviews.summary.line.other": "{breakdown} {count} đánh giá mà không bình luận: 918274242", "reviews.summary.line.other": "{breakdown} {count} đánh giá mà không bình luận: 918274242",
"reviews.summary.lineDay.one": "{count} đánh giá mà không bình luận gì về {when}: {breakdown}", "reviews.summary.lineDay.one": "{count} đánh giá mà không bình luận gì về {when}: {breakdown}",
+1 -1
View File
@@ -282,7 +282,7 @@
"reviews.anonNote": "这是关于这款npub的首条评论,未发现其他相关活动", "reviews.anonNote": "这是关于这款npub的首条评论,未发现其他相关活动",
"reviews.published": "已发布。", "reviews.published": "已发布。",
"reviews.publishedPartial": "已发布至 {total} 继电器中的 {accepted}。", "reviews.publishedPartial": "已发布至 {total} 继电器中的 {accepted}。",
"reviews.propagating": "正在同步,内容可能需要片刻时间才会显示在其他地方。", "reviews.propagating": "正在发布到 Nostr 中继",
"reviews.summary.line.one": "{count} 的评分(无评论):{breakdown}", "reviews.summary.line.one": "{count} 的评分(无评论):{breakdown}",
"reviews.summary.line.other": "{count} 的评分(无评论):{breakdown}", "reviews.summary.line.other": "{count} 的评分(无评论):{breakdown}",
"reviews.summary.lineDay.one": "{count}的评分,未对{when}发表评论:{breakdown}", "reviews.summary.lineDay.one": "{count}的评分,未对{when}发表评论:{breakdown}",
+4
View File
@@ -302,6 +302,10 @@ export async function hydrateMintGrid(options: HydrateOptions): Promise<void> {
const all = await fetchListing(type); const all = await fetchListing(type);
if (!all) return; if (!all) return;
// The reader may have navigated away while the fetch was in flight. The router has
// already swapped this grid out of the document, so rendering into it (and telling the
// caller to re-apply filters against cards nobody can see) is wasted work.
if (!grid.isConnected) return;
const items = limit === undefined ? all : all.slice(0, limit); const items = limit === undefined ? all : all.slice(0, limit);
const cards = renderMintGrid(grid, items, { const cards = renderMintGrid(grid, items, {
+3 -17
View File
@@ -37,16 +37,8 @@ export interface MintRef {
} }
export interface CardOptions { export interface CardOptions {
/** Adds the "propagating to relays" note, for a review published in this session. */ /** Adds the transient publishing note for a review published in this session. */
propagating?: boolean; propagating?: boolean;
/**
* How many relays took it, when the review was published in this session.
*
* Only said out loud when some relay refused: "published to 2 of 4" is a fact the
* author can act on (try again later, and it is already on the network), where a
* clean 4 of 4 is just noise on the card.
*/
publishedTo?: { accepted: number; total: number } | null;
/** Adds a link to the mint being reviewed. */ /** Adds a link to the mint being reviewed. */
mint?: MintRef | null; mint?: MintRef | null;
/** /**
@@ -303,15 +295,9 @@ function actionsHtml(review: LoadedReview, f: Formatters, options: CardOptions):
/* ---------- the card ---------- */ /* ---------- the card ---------- */
export function reviewHtml(review: LoadedReview, f: Formatters, options: CardOptions = {}): string { export function reviewHtml(review: LoadedReview, f: Formatters, options: CardOptions = {}): string {
// The note pulses gently while the review is still in flight. It stops the moment // The note pulses gently for a few seconds after a review is published.
// the relays echo the review back, because then the card is redrawn without it.
const relays = options.publishedTo;
const partial =
relays && relays.accepted < relays.total
? f.t('reviews.publishedPartial', { accepted: relays.accepted, total: relays.total })
: f.t('reviews.published');
const propagating = options.propagating const propagating = options.propagating
? `<p class="rev-note propagating">${escapeHtml(partial)} ${escapeHtml(f.t('reviews.propagating'))}</p>` ? `<p class="rev-note propagating">${escapeHtml(f.t('reviews.propagating'))}</p>`
: ''; : '';
// The anchor id only for real event ids, so `#review-{id}` deep links resolve and // The anchor id only for real event ids, so `#review-{id}` deep links resolve and
+2 -2
View File
@@ -138,7 +138,7 @@ const schema = [
) )
} }
<div class="mint-grid" data-mint-grid> <div class="mint-grid" data-mint-grid="fedimint">
{federations.map((federation, i) => <MintCard mint={federation} rank={i + 1} />)} {federations.map((federation, i) => <MintCard mint={federation} rank={i + 1} />)}
</div> </div>
@@ -259,7 +259,7 @@ const schema = [
const t = useI18n(); const t = useI18n();
const grid = document.querySelector<HTMLElement>('[data-mint-grid]'); const grid = document.querySelector<HTMLElement>('[data-mint-grid="fedimint"]');
const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]'); const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]');
const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]'); const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]'); const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
+2 -2
View File
@@ -142,7 +142,7 @@ const schema = [
{t('lnurlMints.showing', { total: mints.length, online, offline })} {t('lnurlMints.showing', { total: mints.length, online, offline })}
</p> </p>
<div class="mint-grid" data-mint-grid> <div class="mint-grid" data-mint-grid="lnurl">
{mints.map((mint, i) => ( {mints.map((mint, i) => (
<MintCard <MintCard
mint={mint} mint={mint}
@@ -272,7 +272,7 @@ const schema = [
// `t` formats its own numbers, so the grouping separator follows the locale too. // `t` formats its own numbers, so the grouping separator follows the locale too.
const t = useI18n(); const t = useI18n();
const grid = document.querySelector<HTMLElement>('[data-mint-grid]'); const grid = document.querySelector<HTMLElement>('[data-mint-grid="lnurl"]');
const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]'); const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]');
const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]'); const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]'); const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
+2 -2
View File
@@ -125,7 +125,7 @@ const schema = [
{t('mints.showing', { total: mints.length, online, offline })} {t('mints.showing', { total: mints.length, online, offline })}
</p> </p>
<div class="mint-grid" data-mint-grid> <div class="mint-grid" data-mint-grid="cashu">
{mints.map((mint, i) => <MintCard mint={mint} rank={i + 1} capabilities={mint.capabilities} />)} {mints.map((mint, i) => <MintCard mint={mint} rank={i + 1} capabilities={mint.capabilities} />)}
</div> </div>
@@ -244,7 +244,7 @@ const schema = [
// `t` formats its own numbers, so the grouping separator follows the locale too. // `t` formats its own numbers, so the grouping separator follows the locale too.
const t = useI18n(); const t = useI18n();
const grid = document.querySelector<HTMLElement>('[data-mint-grid]'); const grid = document.querySelector<HTMLElement>('[data-mint-grid="cashu"]');
const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]'); const searchInput = document.querySelector<HTMLInputElement>('[data-search-input]');
const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]'); const sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]'); const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
+9
View File
@@ -208,6 +208,15 @@ export function armSharedTransitions(): void {
* only does its work at module scope is dead after the first navigation. It does fire * only does its work at module scope is dead after the first navigation. It does fire
* `astro:page-load` on every arrival, including the first, but on the first it waits * `astro:page-load` on every arrival, including the first, but on the first it waits
* for `window.load`, which is far too late to be the only trigger. Hence both. * for `window.load`, which is far too late to be the only trigger. Hence both.
*
* The listener is never removed, on purpose: a layout island (the language switcher,
* the login dialog) has DOM on every page, and this is what keeps it alive. The flip
* side is that `setup` runs on *every* page the router swaps in, not only pages that
* include the script. A page-level script must therefore look its DOM up by a marker
* unique to that page — `data-mint-grid="cashu"`, `data-home-grid="fedimint"` — and do
* nothing when the marker is absent. A marker shared between pages means a stale page's
* setup finds the current page's DOM and rewrites it: the listing pages once flashed
* back to the previous ecosystem's mints for exactly that reason.
*/ */
export function onReady(setup: () => void): void { export function onReady(setup: () => void): void {
let done = false; let done = false;
+6 -1
View File
@@ -1268,8 +1268,13 @@ main { flex: 1; }
0%, 25% { opacity: .85; } 0%, 25% { opacity: .85; }
100% { opacity: 0; } 100% { opacity: 0; }
} }
/* Still travelling. Stops when the relays echo the review back on the next load. */ /* Visible for a few seconds after a review is published from this tab. */
.rev-note.propagating { animation: soft-pulse 2s var(--ease-in-out) infinite; } .rev-note.propagating { animation: soft-pulse 2s var(--ease-in-out) infinite; }
.rev-note.propagating.is-out {
animation: none;
opacity: 0;
transition: opacity var(--dur-quick) var(--ease-out);
}
/* ---------- status dot ---------- */ /* ---------- status dot ---------- */
.status-dot { position: relative; } .status-dot { position: relative; }
+60
View File
@@ -0,0 +1,60 @@
/**
* The three listing pages must each own their grid.
*
* `<ClientRouter />` swaps pages without reloading, and `onReady` in `scripts/reveal.ts`
* re-runs every page's setup on every arrival — deliberately, since that is what keeps
* the layout islands alive. So once `/mints` has been visited, its setup also runs on
* `/fedimints`. If both pages mark their grid the same way, the stale cashu setup finds
* the fedimint grid, fetches `?type=cashu` and rewrites it: the page shows fedimints for
* a moment, then flashes back to cashu mints. That happened.
*
* The guard is that each page's marker carries its ecosystem, and its script only looks
* for its own. This checks the template, the query and the hydrate call agree, and that
* no bare `data-mint-grid` has crept back in. Plain node, source read as text, like
* `i18n-wiring.test.mjs`.
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const pagesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/pages/[...locale]');
const read = (file) => readFileSync(path.join(pagesDir, file), 'utf8');
const LISTINGS = [
['mints.astro', 'cashu'],
['fedimints.astro', 'fedimint'],
['lnurl-mints.astro', 'lnurl'],
];
for (const [file, type] of LISTINGS) {
test(`${file} marks, queries and hydrates its grid as ${type}`, () => {
const source = read(file);
assert.match(
source,
new RegExp(`<div class="mint-grid" data-mint-grid="${type}">`),
`${file}: template grid is not marked data-mint-grid="${type}"`,
);
assert.match(
source,
new RegExp(`querySelector<HTMLElement>\\('\\[data-mint-grid="${type}"\\]'\\)`),
`${file}: script does not query [data-mint-grid="${type}"]`,
);
assert.match(
source,
new RegExp(`type: '${type}',`),
`${file}: hydrateMintGrid is not called with type '${type}'`,
);
// A bare marker, in either the template or the query, is the bug coming back.
assert.doesNotMatch(source, /data-mint-grid[>\s]/, `${file}: bare data-mint-grid in template`);
assert.doesNotMatch(source, /\[data-mint-grid\]/, `${file}: bare [data-mint-grid] selector`);
});
}
test('the three listings use three different markers', () => {
const types = new Set(LISTINGS.map(([, type]) => type));
assert.equal(types.size, LISTINGS.length);
});