Compare commits
3
Commits
860a4de009
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae7664fbe0 | ||
|
|
70b35f4ccc | ||
|
|
1eade490c8 |
@@ -394,14 +394,28 @@ function escapeText(value: string): string {
|
||||
let pendingPaint = 0;
|
||||
/** Set for one paint after publishing, to mark the new card as it goes in. */
|
||||
let markNewest = false;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** Reviews published in this session, shown at the top until relays echo them back. */
|
||||
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 ---------- */
|
||||
|
||||
@@ -476,8 +490,7 @@ function escapeText(value: string): string {
|
||||
target.innerHTML = slice
|
||||
.map((review) =>
|
||||
reviewHtml(review, f, {
|
||||
propagating: optimistic.has(review.id),
|
||||
publishedTo: optimistic.get(review.id) ?? null,
|
||||
propagating: publishingNotes.has(review.id),
|
||||
permalink,
|
||||
}),
|
||||
)
|
||||
@@ -667,10 +680,7 @@ function escapeText(value: string): string {
|
||||
* that they would have seen had they written it on this page.
|
||||
*/
|
||||
if (handedForward && !all.some((review) => review.id === handedForward.id)) {
|
||||
optimistic.set(handedForward.id, {
|
||||
accepted: handedForward.accepted,
|
||||
total: handedForward.total,
|
||||
});
|
||||
markOptimistic(handedForward.id, handedForward.accepted, handedForward.total);
|
||||
all.unshift({
|
||||
id: handedForward.id,
|
||||
pubkey: handedForward.pubkey,
|
||||
@@ -810,7 +820,7 @@ function escapeText(value: string): string {
|
||||
},
|
||||
onPublished: (published) => {
|
||||
// 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({
|
||||
id: published.id,
|
||||
pubkey: published.pubkey,
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "دي أول مراجعة من الـnpub ده، ومفيش نشاط تاني ليه",
|
||||
"reviews.published": "نُشرت",
|
||||
"reviews.publishedPartial": "نُشرت إلى {accepted} من {total} إعادة شحن.",
|
||||
"reviews.propagating": "البروغات، قد يستغرق الأمر لحظة للظهور في مكان آخر.",
|
||||
"reviews.propagating": "بننشر على موزّعات Nostr",
|
||||
"reviews.summary.line.one": "{count} تصنيف دون تعليق: {breakdown}",
|
||||
"reviews.summary.line.other": "{count} تصنيف دون تعليق: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} تصنيف دون تعليق على {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "أول تقييم من هذا npub، لم يتم العثور على أي نشاط آخر",
|
||||
"reviews.published": "تم النشر.",
|
||||
"reviews.publishedPartial": "تم النشر إلى {accepted} من أصل {total} مرحل.",
|
||||
"reviews.propagating": "أثناء النشر، قد يستغرق الأمر لحظة حتى يظهر في مكان آخر.",
|
||||
"reviews.propagating": "جارٍ النشر إلى مرحلات Nostr",
|
||||
"reviews.summary.line.one": "{count} تقييم بدون تعليق: {breakdown}",
|
||||
"reviews.summary.line.other": "{count} تقييمًا بدون تعليق: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "تقييم {count} بدون تعليق على {when}: {breakdown}",
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
"reviews.anonNote": "První přezkum z tohoto npub, žádná jiná činnost nenalezena",
|
||||
"reviews.published": "Publikováno.",
|
||||
"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.other": "{count}ratingy bez komentáře:{breakdown}",
|
||||
"reviews.summary.line.few": "{count}ratingy bez komentáře:{breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Første anmeldelse af denne npub; der er ikke fundet andre aktiviteter",
|
||||
"reviews.published": "Udgivet.",
|
||||
"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.other": "{count}-vurderinger uden kommentar: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count}-vurdering uden kommentar til {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Erste Rezension von diesem Verlag, keine weiteren Aktivitäten gefunden",
|
||||
"reviews.published": "Veröffentlicht.",
|
||||
"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.other": "{count}-Bewertungen ohne Kommentar: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count}-Bewertung ohne Kommentar auf {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Πρώτη κριτική από αυτό το npub, δεν βρέθηκε άλλη δραστηριότητα",
|
||||
"reviews.published": "Δημοσίευση.",
|
||||
"reviews.publishedPartial": "Δημοσίευση στο{accepted} του{total} ⁇ λεμάν.",
|
||||
"reviews.propagating": "Διαδοχικά, μπορεί να πάρει μια στιγμή για να εμφανιστεί αλλού.",
|
||||
"reviews.propagating": "Δημοσίευση στα relays του Nostr",
|
||||
"reviews.summary.line.one": "{count} αξιολόγηση χωρίς σχόλιο:{breakdown}",
|
||||
"reviews.summary.line.other": "{count} αξιολογήσεις χωρίς σχόλιο:{breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} αξιολόγηση χωρίς σχόλιο σχετικά με{when}:{breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "First review from this npub, no other activity found",
|
||||
"reviews.published": "Published.",
|
||||
"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.other": "{count} ratings without a comment: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} rating without a comment on {when}: {breakdown}",
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
"reviews.anonNote": "Primera reseña de este npub, no se ha encontrado ninguna otra actividad",
|
||||
"reviews.published": "Publicada.",
|
||||
"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.other": "{count} valoraciones sin comentario: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} valoración sin comentario del {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Premier avis sur ce npub, aucune autre activité trouvée",
|
||||
"reviews.published": "Publié.",
|
||||
"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.other": "Notes {count} sans commentaire : {breakdown}",
|
||||
"reviews.summary.lineDay.one": "Note {count} sans commentaire sur {when} : {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "इस npub से पहली समीक्षा, कोई अन्य गतिविधि नहीं मिली।",
|
||||
"reviews.published": "प्रकाशित",
|
||||
"reviews.publishedPartial": "{total} रिलेज़ में से {accepted} पर प्रकाशित।",
|
||||
"reviews.propagating": "प्रसारित करते समय, कहीं और प्रकट होने में थोड़ा समय लग सकता है।",
|
||||
"reviews.propagating": "Nostr रिले पर प्रकाशित किया जा रहा है",
|
||||
"reviews.summary.line.one": "बिना टिप्पणी के {count} रेटिंग: {breakdown}",
|
||||
"reviews.summary.line.other": "टिप्पणी के बिना रेटिंग: {count}; रेटिंग: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} रेटिंग, {when} पर बिना टिप्पणी के: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Ulasan pertama dari npub ini, tidak ada aktivitas lain ditemukan",
|
||||
"reviews.published": "Diterbitkan.",
|
||||
"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.other": "Rating {count} tanpa komentar: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} rating tanpa komentar di {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Prima recensione da questo npub, nessun'altra attività trovata",
|
||||
"reviews.published": "Pubblicato.",
|
||||
"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.other": "{count} valutazioni senza un commento: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} valutazione senza un commento su {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "このnpubからの最初のレビュー、他の活動が見つかりません",
|
||||
"reviews.published": "掲載情報",
|
||||
"reviews.publishedPartial": "に公開 {accepted} の {total} リレー.",
|
||||
"reviews.propagating": "伝播は、他の場所で出現する瞬間を取るかもしれません。",
|
||||
"reviews.propagating": "Nostrリレーに公開中",
|
||||
"reviews.summary.line.one": "{count} コメントのない評価: {breakdown}",
|
||||
"reviews.summary.line.other": "{count} コメントのない評価: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} 評価なし にコメント {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Eerste review van deze npub, verder geen activiteit gevonden",
|
||||
"reviews.published": "Gepubliceerd.",
|
||||
"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.other": "{count} beoordelingen zonder tekst: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count} beoordeling zonder tekst op {when}: {breakdown}",
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
"reviews.anonNote": "Pierwsza recenzja z tego npubu, nie znaleziono żadnej innej aktywności",
|
||||
"reviews.published": "Opublikowano.",
|
||||
"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.few": "{count} oceny bez komentarza: {breakdown}",
|
||||
"reviews.summary.line.many": "{count} ocen bez komentarza: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Primeira revisão deste npub, nenhuma outra atividade encontrada",
|
||||
"reviews.published": "Publicado.",
|
||||
"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.other": "Avaliações {count} sem comentários: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "Classificação {count} sem comentários em {when}: {breakdown}",
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
"reviews.anonNote": "Prima revizuire a acestei npub-uri, nicio altă activitate nu a fost găsită",
|
||||
"reviews.published": "Publicat.",
|
||||
"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.other": "{count} ratinguri fără comentarii:{breakdown}",
|
||||
"reviews.summary.line.few": "{count} ratinguri fără comentarii:{breakdown}",
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
"reviews.anonNote": "Первый отзыв с этого npub, другой активности не найдено",
|
||||
"reviews.published": "Опубликовано.",
|
||||
"reviews.publishedPartial": "Опубликовано на {accepted} из {total} relays.",
|
||||
"reviews.propagating": "Распространяется по сети, в других местах отзыв может появиться не сразу.",
|
||||
"reviews.propagating": "Публикация на relays Nostr",
|
||||
"reviews.summary.line.one": "Рейтинг «{count}» без комментариев: {breakdown}",
|
||||
"reviews.summary.line.other": "Рейтинги «{count}» без комментариев: {breakdown}",
|
||||
"reviews.summary.line.few": "Рейтинги «{count}» без комментариев: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Första recensionen av denna npub, inga andra aktiviteter har hittats",
|
||||
"reviews.published": "Publicerad.",
|
||||
"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.other": "{count}-betyg utan kommentar: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "Betyg för {count} utan kommentar om {when}: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "Bu npub'dan ilk inceleme, başka bir faaliyet bulunmadı",
|
||||
"reviews.published": "Yayınlandı.",
|
||||
"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.other": "{count} yorumsuz puanlar: {breakdown}",
|
||||
"reviews.summary.lineDay.one": "{when} yorumsuz {count} puanı: {breakdown}",
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
"reviews.anonNote": "Перший відгук від цього npub, іншої активності не знайдено",
|
||||
"reviews.published": "Опубліковано.",
|
||||
"reviews.publishedPartial": "Опубліковано на {accepted} із {total} relays.",
|
||||
"reviews.propagating": "Поширюється мережею, поява в інших місцях може тривати деякий час.",
|
||||
"reviews.propagating": "Публікація на relays Nostr",
|
||||
"reviews.summary.line.one": "{count} рейтинг без коментаря: {breakdown}",
|
||||
"reviews.summary.line.few": "{count} оцінки без коментарів: {breakdown}",
|
||||
"reviews.summary.line.many": "{count} оцінок без коментарів: {breakdown}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "اس ناول سے پہلا جائزہ، کوئی اور سرگرمی نہیں پائی گئی",
|
||||
"reviews.published": "شائع ہوا۔",
|
||||
"reviews.publishedPartial": "شائع شدہ{accepted}کا مطلب{total}ریلویز.",
|
||||
"reviews.propagating": "اِس لئے شاید آپ کسی اَور ملک میں جا کر اُس کے بارے میں بات کریں ۔",
|
||||
"reviews.propagating": "Nostr ریلے پر شائع ہو رہا ہے",
|
||||
"reviews.summary.line.one": "{count}بغیر تبصرے کے شرحیں:{breakdown}",
|
||||
"reviews.summary.line.other": "{count}بغیر تبصرے کے شرحیں:{breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count}بغیر تبصرے کے خواندگی{when}:{breakdown}",
|
||||
|
||||
@@ -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.published": "Xuất bản.",
|
||||
"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.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}",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"reviews.anonNote": "这是关于这款npub的首条评论,未发现其他相关活动",
|
||||
"reviews.published": "已发布。",
|
||||
"reviews.publishedPartial": "已发布至 {total} 继电器中的 {accepted}。",
|
||||
"reviews.propagating": "正在同步,内容可能需要片刻时间才会显示在其他地方。",
|
||||
"reviews.propagating": "正在发布到 Nostr 中继",
|
||||
"reviews.summary.line.one": "{count} 的评分(无评论):{breakdown}",
|
||||
"reviews.summary.line.other": "{count} 的评分(无评论):{breakdown}",
|
||||
"reviews.summary.lineDay.one": "{count}的评分,未对{when}发表评论:{breakdown}",
|
||||
|
||||
@@ -302,6 +302,10 @@ export async function hydrateMintGrid(options: HydrateOptions): Promise<void> {
|
||||
|
||||
const all = await fetchListing(type);
|
||||
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 cards = renderMintGrid(grid, items, {
|
||||
|
||||
+16
-28
@@ -55,19 +55,6 @@ export interface BuildReview {
|
||||
* characters or matching a common test string do not qualify. They still appear in
|
||||
* full on the mint page, nothing is hidden, this strip is just a shop window.
|
||||
*/
|
||||
/** How many candidates to gather per card shown, so the ordering has a real choice. */
|
||||
const POOL_FACTOR = 12;
|
||||
|
||||
/**
|
||||
* How recent a review must still be to be promoted for having a named author.
|
||||
*
|
||||
* The ordering below prefers reviews whose author has a kind 0, and this is the leash
|
||||
* on that preference. Without it the strip would headline a named review from two
|
||||
* years ago under a heading that says "Latest reviews"; with it, a named review from
|
||||
* within the season can lead and anything older cannot.
|
||||
*/
|
||||
const PROMOTE_MAX_AGE_S = 90 * 24 * 60 * 60;
|
||||
|
||||
const TEST_STRINGS = new Set([
|
||||
'test', 'testing', 'test test', 'hello', 'hi', 'ok', 'okay', 'good', 'nice',
|
||||
'great', 'cool', 'gm', 'a', '.', '..', '...', 'asdf', 'qwerty', '123',
|
||||
@@ -227,29 +214,30 @@ export async function fetchLatestReviews(
|
||||
profile: null,
|
||||
});
|
||||
|
||||
if (candidates.length >= limit * POOL_FACTOR) break;
|
||||
// The scan runs newest first, so the first `limit` survivors are the latest
|
||||
// `limit` reviews. Nothing further down the list can outrank them.
|
||||
if (candidates.length >= limit) break;
|
||||
}
|
||||
|
||||
const profiles = await fetchProfiles(candidates.map((c) => c.pubkey));
|
||||
for (const review of candidates) review.profile = profiles.get(review.pubkey) ?? null;
|
||||
|
||||
/*
|
||||
* Recent reviews with a named author come first; everything else stays newest
|
||||
* first behind them.
|
||||
* Strict recency, newest first, as the last thing that happens to this list.
|
||||
*
|
||||
* Most review keys have published exactly one event in their life: the review
|
||||
* itself. Nothing can be fetched for them, so a strict recency order fills this
|
||||
* strip with anonymous npubs while a named review sits a few rows below the
|
||||
* cut. This is the home page shop window, which already drops junk bodies, so
|
||||
* it prefers a review a reader can attach a person to. Nothing is hidden:
|
||||
* every review is on its mint page, and PROMOTE_MAX_AGE_S keeps "latest"
|
||||
* meaning latest.
|
||||
* The heading says "Latest reviews", so the order under it is the event
|
||||
* `created_at` and nothing else. This used to float reviews whose author had a
|
||||
* kind 0 to the front, which read as broken on the page: a named review from two
|
||||
* months ago sat between two reviews from this week, and the card feet said so,
|
||||
* because the "2mo ago" label is formatted from the very same `created_at` this
|
||||
* sorts on. Author names are still resolved and still shown — they just no longer
|
||||
* decide the order.
|
||||
*
|
||||
* Sorted here rather than left to the scan above, so that the invariant holds
|
||||
* whatever the gathering loop does later, and `id` breaks ties between two events
|
||||
* that share a second so two builds of the same events agree.
|
||||
*/
|
||||
const cutoff = Math.floor(Date.now() / 1000) - PROMOTE_MAX_AGE_S;
|
||||
const promoted = (review: BuildReview): number =>
|
||||
review.profile?.found && review.created_at >= cutoff ? 1 : 0;
|
||||
|
||||
candidates.sort((a, b) => promoted(b) - promoted(a) || b.created_at - a.created_at);
|
||||
candidates.sort((a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id));
|
||||
|
||||
return candidates.slice(0, limit);
|
||||
} catch {
|
||||
|
||||
@@ -37,16 +37,8 @@ export interface MintRef {
|
||||
}
|
||||
|
||||
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;
|
||||
/**
|
||||
* 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. */
|
||||
mint?: MintRef | null;
|
||||
/**
|
||||
@@ -303,15 +295,9 @@ function actionsHtml(review: LoadedReview, f: Formatters, options: CardOptions):
|
||||
/* ---------- the card ---------- */
|
||||
|
||||
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 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');
|
||||
// The note pulses gently for a few seconds after a review is published.
|
||||
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
|
||||
|
||||
@@ -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} />)}
|
||||
</div>
|
||||
|
||||
@@ -259,7 +259,7 @@ const schema = [
|
||||
|
||||
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 sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
|
||||
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
|
||||
|
||||
@@ -142,7 +142,7 @@ const schema = [
|
||||
{t('lnurlMints.showing', { total: mints.length, online, offline })}
|
||||
</p>
|
||||
|
||||
<div class="mint-grid" data-mint-grid>
|
||||
<div class="mint-grid" data-mint-grid="lnurl">
|
||||
{mints.map((mint, i) => (
|
||||
<MintCard
|
||||
mint={mint}
|
||||
@@ -272,7 +272,7 @@ const schema = [
|
||||
// `t` formats its own numbers, so the grouping separator follows the locale too.
|
||||
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 sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
|
||||
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
|
||||
|
||||
@@ -125,7 +125,7 @@ const schema = [
|
||||
{t('mints.showing', { total: mints.length, online, offline })}
|
||||
</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} />)}
|
||||
</div>
|
||||
|
||||
@@ -244,7 +244,7 @@ const schema = [
|
||||
// `t` formats its own numbers, so the grouping separator follows the locale too.
|
||||
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 sortSelect = document.querySelector<HTMLSelectElement>('[data-sort]');
|
||||
const hideOffline = document.querySelector<HTMLButtonElement>('[data-hide-offline]');
|
||||
|
||||
@@ -208,6 +208,15 @@ export function armSharedTransitions(): void {
|
||||
* 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
|
||||
* 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 {
|
||||
let done = false;
|
||||
|
||||
@@ -1268,8 +1268,13 @@ main { flex: 1; }
|
||||
0%, 25% { opacity: .85; }
|
||||
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.is-out {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--dur-quick) var(--ease-out);
|
||||
}
|
||||
|
||||
/* ---------- status dot ---------- */
|
||||
.status-dot { position: relative; }
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user