fix(challenge modal): open iframe consent directly

This commit is contained in:
Tommaso Casaburi
2026-04-30 18:56:51 +07:00
parent 5c74a1e495
commit 897e170485
38 changed files with 278 additions and 122 deletions
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "لا. شراء pass لن يجعل شخصًا ما moderator أو admin أو مالكًا لأي board. إنه شراء داعم، وليس تجاوزًا للحوكمة.",
"more_threads_last_week": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الأسبوع الماضي</1>]",
"more_threads_last_month": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الشهر الماضي</1>]",
"more_threads_last_year": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من السنة الماضية</1>]"
"more_threads_last_year": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من السنة الماضية</1>]",
"iframe_challenge_confirm": "{{board}} يريد فتح {{site}}.\n\nبالنسبة إلى {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "না। একটি pass কিনলে কেউ moderator, admin, বা কোনো board-এর মালিক হবে না। এটি সমর্থকদের জন্য একটি ক্রয়, শাসনব্যবস্থাকে অগ্রাহ্য করার উপায় নয়।",
"more_threads_last_week": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত সপ্তাহের আরও থ্রেড দেখান</1>]",
"more_threads_last_month": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত মাসের আরও থ্রেড দেখান</1>]",
"more_threads_last_year": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত বছরের আরও থ্রেড দেখান</1>]"
"more_threads_last_year": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত বছরের আরও থ্রেড দেখান</1>]",
"iframe_challenge_confirm": "{{board}} {{site}} খুলতে চায়।\n\n{{publicationType}}-এর জন্য: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Ne. Koupě pass z nikoho neudělá moderator, admin ani vlastníka board. Je to podpůrný nákup, ne obejití governance.",
"more_threads_last_week": "{{count}} vláken od {{currentTimeFilterName}} — [<1>Ukázat více vláken z minulého týdne</1>]",
"more_threads_last_month": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého měsíce</1>]",
"more_threads_last_year": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého roku</1>]"
"more_threads_last_year": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého roku</1>]",
"iframe_challenge_confirm": "{{board}} chce otevřít {{site}}.\n\nPro {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nej. At købe et pass gør ikke nogen til moderator, admin eller ejer af et board. Det er et støttekøb, ikke en tilsidesættelse af governance.",
"more_threads_last_week": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste uge</1>]",
"more_threads_last_month": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste måned</1>]",
"more_threads_last_year": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste år</1>]"
"more_threads_last_year": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste år</1>]",
"iframe_challenge_confirm": "{{board}} vil åbne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nein. Der Kauf eines Passes macht niemanden zu einem Moderator, Admin oder Eigentümer eines Boards. Es ist ein Unterstützerkauf, keine Umgehung der Governance.",
"more_threads_last_week": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads aus der letzten Woche anzeigen</1>]",
"more_threads_last_month": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Monat anzeigen</1>]",
"more_threads_last_year": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Jahr anzeigen</1>]"
"more_threads_last_year": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Jahr anzeigen</1>]",
"iframe_challenge_confirm": "{{board}} möchte {{site}} öffnen.\n\nFür {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Όχι. Η αγορά ενός pass δεν θα κάνει κάποιον συντονιστή, διαχειριστή ή ιδιοκτήτη ενός board. Είναι μια αγορά υποστήριξης, όχι παράκαμψη της διακυβέρνησης.",
"more_threads_last_week": "{{count}} θέματα από {{currentTimeFilterName}} — [<1>Εμφάνιση περισσότερων θεμάτων από την περασμένη εβδομάδα</1>]",
"more_threads_last_month": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από τον τελευταίο μήνα</1>]",
"more_threads_last_year": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από το περασμένο έτος</1>]"
"more_threads_last_year": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από το περασμένο έτος</1>]",
"iframe_challenge_confirm": "Το {{board}} θέλει να ανοίξει το {{site}}.\n\nΓια {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "No. Buying a pass will not make someone a moderator, an admin, or the owner of a board. It is a supporter purchase, not a governance override.",
"more_threads_last_week": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Week</1>]",
"more_threads_last_month": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Month</1>]",
"more_threads_last_year": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Year</1>]"
"more_threads_last_year": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Year</1>]",
"iframe_challenge_confirm": "{{board}} wants to open {{site}}.\n\nFor {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "No. Comprar un pass no convertirá a alguien en moderator, admin ni propietario de un board. Es una compra de apoyo, no una anulación de la gobernanza.",
"more_threads_last_week": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Mostrar más hilos de la semana pasada</1>]",
"more_threads_last_month": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del mes pasado</1>]",
"more_threads_last_year": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del año pasado</1>]"
"more_threads_last_year": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del año pasado</1>]",
"iframe_challenge_confirm": "{{board}} quiere abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "خیر. خرید یک pass کسی را به moderator، admin یا مالک یک board تبدیل نمی‌کند. این یک خرید حمایتی است، نه راهی برای دور زدن حاکمیت.",
"more_threads_last_week": "{{count}} موضوع از {{currentTimeFilterName}} — [<1>نمایش موضوعات بیشتر از هفته گذشته</1>]",
"more_threads_last_month": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های ماه گذشته</1>]",
"more_threads_last_year": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های سال گذشته</1>]"
"more_threads_last_year": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های سال گذشته</1>]",
"iframe_challenge_confirm": "{{board}} می‌خواهد {{site}} را باز کند.\n\nبرای {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Ei. pass ostaminen ei tee kenestäkään moderator, admin tai board omistajaa. Se on tukijaosto, ei governance-mallin ohitus.",
"more_threads_last_week": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää ketjuja viime viikolta</1>]",
"more_threads_last_month": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime kuun ketjuja</1>]",
"more_threads_last_year": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime vuoden ketjuja</1>]"
"more_threads_last_year": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime vuoden ketjuja</1>]",
"iframe_challenge_confirm": "{{board}} haluaa avata sivuston {{site}}.\n\nKohteelle {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Hindi. Ang pagbili ng pass ay hindi gagawing moderator, admin, o may-ari ng isang board ang sinuman. Isa itong supporter purchase, hindi governance override.",
"more_threads_last_week": "{{count}} mga thread mula sa {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula sa nakaraang linggo</1>]",
"more_threads_last_month": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang buwan</1>]",
"more_threads_last_year": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang taon</1>]"
"more_threads_last_year": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang taon</1>]",
"iframe_challenge_confirm": "Nais buksan ng {{board}} ang {{site}}.\n\nPara sa {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Non. Acheter un pass ne fera de personne un moderator, un admin ou le propriétaire d'un board. C'est un achat de soutien, pas un contournement de la gouvernance.",
"more_threads_last_week": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de la semaine dernière</1>]",
"more_threads_last_month": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils du mois dernier</1>]",
"more_threads_last_year": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de l'année dernière</1>]"
"more_threads_last_year": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de l'année dernière</1>]",
"iframe_challenge_confirm": "{{board}} veut ouvrir {{site}}.\n\nPour {{publicationType}} : {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "לא. קניית pass לא תהפוך מישהו ל-moderator, ל-admin או לבעלים של board. זו רכישת תמיכה, לא עקיפה של הממשל.",
"more_threads_last_week": "{{count}} אשכולות מאז {{currentTimeFilterName}} — [<1>הצג עוד אשכולות מהשבוע שעבר</1>]",
"more_threads_last_month": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהחודש שעבר</1>]",
"more_threads_last_year": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהשנה שעברה</1>]"
"more_threads_last_year": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהשנה שעברה</1>]",
"iframe_challenge_confirm": "{{board}} רוצה לפתוח את {{site}}.\n\nעבור {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "नहीं। pass खरीदने से कोई moderator, admin या किसी board का मालिक नहीं बन जाएगा। यह एक समर्थक खरीद है, शासन को दरकिनार करने का तरीका नहीं।",
"more_threads_last_week": "{{count}} थ्रेड {{currentTimeFilterName}} से — [<1>पिछले सप्ताह के और थ्रेड दिखाएं</1>]",
"more_threads_last_month": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>अगले महीने के अधिक थ्रेड्स दिखाएं</1>]",
"more_threads_last_year": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>पिछले साल के अधिक थ्रेड्स दिखाएं</1>]"
"more_threads_last_year": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>पिछले साल के अधिक थ्रेड्स दिखाएं</1>]",
"iframe_challenge_confirm": "{{board}} {{site}} खोलना चाहता है।\n\n{{publicationType}} के लिए: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nem. A pass megvásárlásától senki sem lesz moderator, admin vagy egy board tulajdonosa. Ez egy támogatói vásárlás, nem a governance felülírása.",
"more_threads_last_week": "{{count}} szál a {{currentTimeFilterName}} óta — [<1>Mutass több szálat a múlt héten</1>]",
"more_threads_last_month": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt hónapból</1>]",
"more_threads_last_year": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt évből</1>]"
"more_threads_last_year": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt évből</1>]",
"iframe_challenge_confirm": "{{board}} meg szeretné nyitni a következőt: {{site}}.\n\n{{publicationType}} esetén: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Tidak. Membeli pass tidak akan menjadikan seseorang moderator, admin, atau pemilik sebuah board. Ini adalah pembelian dukungan, bukan pengesampingan tata kelola.",
"more_threads_last_week": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Show lebih banyak thread dari minggu lalu</1>]",
"more_threads_last_month": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari bulan lalu</1>]",
"more_threads_last_year": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari tahun lalu</1>]"
"more_threads_last_year": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari tahun lalu</1>]",
"iframe_challenge_confirm": "{{board}} ingin membuka {{site}}.\n\nUntuk {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "No. Acquistare un pass non renderà qualcuno un moderator, un admin o il proprietario di una board. È un acquisto di supporto, non un aggiramento della governance.",
"more_threads_last_week": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra più thread dalla scorsa settimana</1>]",
"more_threads_last_month": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread del mese scorso</1>]",
"more_threads_last_year": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread dell'anno scorso</1>]"
"more_threads_last_year": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread dell'anno scorso</1>]",
"iframe_challenge_confirm": "{{board}} vuole aprire {{site}}.\n\nPer {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "いいえ。pass を購入しても、moderator、admin、または board の所有者になることはありません。これは支援のための購入であり、ガバナンスを上書きするものではありません。",
"more_threads_last_week": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先週のスレッドをさらに表示</1>]",
"more_threads_last_month": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先月のスレッドをさらに表示</1>]",
"more_threads_last_year": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>去年のスレッドをさらに表示</1>]"
"more_threads_last_year": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>去年のスレッドをさらに表示</1>]",
"iframe_challenge_confirm": "{{board}}が{{site}}を開こうとしています。\n\n{{publicationType}}の場合: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "아니요. pass를 구매해도 누군가가 moderator, admin 또는 board 소유자가 되지는 않습니다. 이것은 후원 구매이지, 거버넌스 무효화 수단이 아닙니다.",
"more_threads_last_week": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 주의 더 많은 스레드 보기</1>]",
"more_threads_last_month": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 달의 스레드 더 보기</1>]",
"more_threads_last_year": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>작년의 스레드 더 보기</1>]"
"more_threads_last_year": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>작년의 스레드 더 보기</1>]",
"iframe_challenge_confirm": "{{board}}에서 {{site}} 열기를 요청했습니다.\n\n{{publicationType}}의 경우: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "नाही. pass खरेदी केल्याने कोणी moderator, admin किंवा एखाद्या boardचा मालक होणार नाही. ही समर्थक खरेदी आहे, शासनाला बगल देण्याचा मार्ग नाही.",
"more_threads_last_week": "{{count}} थ्रेड {{currentTimeFilterName}} पासून — [<1>गेल्या आठवड्यातील अधिक थ्रेड्स पहा</1>]",
"more_threads_last_month": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या महिन्यातील आणखी थ्रेड्स दाखवा</1>]",
"more_threads_last_year": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या वर्षातील आणखी थ्रेड्स दाखवा</1>]"
"more_threads_last_year": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या वर्षातील आणखी थ्रेड्स दाखवा</1>]",
"iframe_challenge_confirm": "{{board}} ला {{site}} उघडायचे आहे.\n\n{{publicationType}} साठी: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nee. Door een pass te kopen wordt iemand geen moderator, admin of eigenaar van een board. Het is een ondersteunende aankoop, geen governance-override.",
"more_threads_last_week": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Toon meer threads van vorige week</1>]",
"more_threads_last_month": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen maand tonen</1>]",
"more_threads_last_year": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen jaar tonen</1>]"
"more_threads_last_year": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen jaar tonen</1>]",
"iframe_challenge_confirm": "{{board}} wil {{site}} openen.\n\nVoor {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nei. Å kjøpe et pass gjør ikke noen til moderator, admin eller eier av et board. Det er et støttekjøp, ikke en overstyring av styringen.",
"more_threads_last_week": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige uke</1>]",
"more_threads_last_month": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige måned</1>]",
"more_threads_last_year": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige år</1>]"
"more_threads_last_year": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige år</1>]",
"iframe_challenge_confirm": "{{board}} vil åpne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nie. Kupienie pass nie uczyni nikogo moderator, admin ani właścicielem board. To zakup wspierający, a nie obejście zasad governance.",
"more_threads_last_week": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego tygodnia</1>]",
"more_threads_last_month": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego miesiąca</1>]",
"more_threads_last_year": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego roku</1>]"
"more_threads_last_year": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego roku</1>]",
"iframe_challenge_confirm": "{{board}} chce otworzyć {{site}}.\n\nDla {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Não. Comprar um pass não fará de alguém um moderator, um admin ou o dono de um board. É uma compra de apoio, não uma substituição da governança.",
"more_threads_last_week": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos da semana passada</1>]",
"more_threads_last_month": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do mês passado</1>]",
"more_threads_last_year": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do ano passado</1>]"
"more_threads_last_year": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do ano passado</1>]",
"iframe_challenge_confirm": "{{board}} quer abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nu. Cumpărarea unui pass nu va face pe cineva moderator, admin sau proprietarul unui board. Este o achiziție de susținere, nu o anulare a guvernanței.",
"more_threads_last_week": "{{count}} fire de discuție de la {{currentTimeFilterName}} — [<1>Arată mai multe fire din săptămâna trecută</1>]",
"more_threads_last_month": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de luna trecută</1>]",
"more_threads_last_year": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de anul trecut</1>]"
"more_threads_last_year": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de anul trecut</1>]",
"iframe_challenge_confirm": "{{board}} vrea să deschidă {{site}}.\n\nPentru {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Нет. Покупка pass не сделает кого-либо moderator, admin или владельцем board. Это покупка в поддержку проекта, а не обход системы управления.",
"more_threads_last_week": "{{count}} тредов с {{currentTimeFilterName}} — [<1>Показать больше тредов за прошлую неделю</1>]",
"more_threads_last_month": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого месяца</1>]",
"more_threads_last_year": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого года</1>]"
"more_threads_last_year": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого года</1>]",
"iframe_challenge_confirm": "{{board}} хочет открыть {{site}}.\n\nДля {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Jo. Blerja e një pass nuk do ta bëjë dikë moderator, admin ose pronar të një board-i. Është një blerje mbështetëse, jo një anashkalim i qeverisjes.",
"more_threads_last_week": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga java e kaluar</1>]",
"more_threads_last_month": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga muaji i kaluar</1>]",
"more_threads_last_year": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga viti i kaluar</1>]"
"more_threads_last_year": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga viti i kaluar</1>]",
"iframe_challenge_confirm": "{{board}} dëshiron të hapë {{site}}.\n\nPër {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Nej. Att köpa ett pass gör inte någon till moderator, admin eller ägare av en board. Det är ett stödköp, inte en åsidosättning av governance.",
"more_threads_last_week": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra veckan</1>]",
"more_threads_last_month": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra månaden</1>]",
"more_threads_last_year": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra året</1>]"
"more_threads_last_year": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra året</1>]",
"iframe_challenge_confirm": "{{board}} vill öppna {{site}}.\n\nFör {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "కాదు. ఒక pass కొనడం వల్ల ఎవరూ moderator, admin లేదా ఏదైనా board యజమాని కాలేరు. ఇది మద్దతుదారుల కొనుగోలు మాత్రమే, పాలనను దాటవేయడం కాదు.",
"more_threads_last_week": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత వారానికి మరిన్ని థ్రెడ్‌లు చూపించు</1>]",
"more_threads_last_month": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత నెల నుండి మరిన్ని థ్రెడ్‌లను చూపించండి</1>]",
"more_threads_last_year": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత సంవత్సరం నుండి మరిన్ని థ్రెడ్‌లను చూపించండి</1>]"
"more_threads_last_year": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత సంవత్సరం నుండి మరిన్ని థ్రెడ్‌లను చూపించండి</1>]",
"iframe_challenge_confirm": "{{board}} {{site}} ను తెరవాలని కోరుతోంది.\n\n{{publicationType}} కోసం: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "ไม่ การซื้อ pass จะไม่ทำให้ใครกลายเป็น moderator, admin หรือเจ้าของ board นี่คือการซื้อเพื่อสนับสนุน ไม่ใช่การลบล้างการกำกับดูแล",
"more_threads_last_week": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้เพิ่มเติมจากสัปดาห์ที่แล้ว</1>]",
"more_threads_last_month": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากเดือนที่แล้วเพิ่มเติม</1>]",
"more_threads_last_year": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากปีที่แล้วเพิ่มเติม</1>]"
"more_threads_last_year": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากปีที่แล้วเพิ่มเติม</1>]",
"iframe_challenge_confirm": "{{board}} ต้องการเปิด {{site}}\n\nสำหรับ {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Hayır. Bir pass satın almak kimseyi moderator, admin ya da bir boardun sahibi yapmaz. Bu bir destekçi satın alımıdır, governance'i geçersiz kılma yolu değildir.",
"more_threads_last_week": "{{count}} başlık {{currentTimeFilterName}}den itibaren — [<1>Geçen haftadan daha fazla başlık göster</1>]",
"more_threads_last_month": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen aydan daha fazla konu göster</1>]",
"more_threads_last_year": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen yıldan daha fazla konu göster</1>]"
"more_threads_last_year": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen yıldan daha fazla konu göster</1>]",
"iframe_challenge_confirm": "{{board}}, {{site}} sitesini açmak istiyor.\n\n{{publicationType}} için: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Ні. Купівля pass не зробить когось moderator, admin або власником board. Це покупка на підтримку, а не спосіб обійти систему врядування.",
"more_threads_last_week": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого тижня</1>]",
"more_threads_last_month": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого місяця</1>]",
"more_threads_last_year": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого року</1>]"
"more_threads_last_year": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого року</1>]",
"iframe_challenge_confirm": "{{board}} хоче відкрити {{site}}.\n\nДля {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "نہیں۔ pass خریدنے سے کوئی شخص moderator، admin، یا کسی board کا مالک نہیں بنے گا۔ یہ حمایتی خرید ہے، گورننس کو نظرانداز کرنے کا طریقہ نہیں۔",
"more_threads_last_week": "{{count}} تھریڈ {{currentTimeFilterName}} سے — [<1>گزشتہ ہفتے کے مزید تھریڈ دیکھیں</1>]",
"more_threads_last_month": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>پچھلے ماہ کے مزید تھریڈز دکھائیں</1>]",
"more_threads_last_year": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>گزشتہ سال کے مزید تھریڈز دکھائیں</1>]"
"more_threads_last_year": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>گزشتہ سال کے مزید تھریڈز دکھائیں</1>]",
"iframe_challenge_confirm": "{{board}}، {{site}} کھولنا چاہتا ہے۔\n\n{{publicationType}} کے لیے: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "Không. Mua pass sẽ không biến ai đó thành moderator, admin hoặc chủ sở hữu của một board. Đây là một khoản mua để ủng hộ, không phải cách ghi đè cơ chế quản trị.",
"more_threads_last_week": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Xem thêm chủ đề từ tuần trước</1>]",
"more_threads_last_month": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ tháng trước</1>]",
"more_threads_last_year": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ năm ngoái</1>]"
"more_threads_last_year": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ năm ngoái</1>]",
"iframe_challenge_confirm": "{{board}} muốn mở {{site}}.\n\nDành cho {{publicationType}}: {{excerpt}}"
}
+2 -1
View File
@@ -363,5 +363,6 @@
"pass_answer_privileges": "不会。购买 pass 并不会让任何人成为版主、管理员或某个 board 的所有者。这是一种支持性购买,不是对治理规则的覆盖。",
"more_threads_last_week": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多来自上周的主题</1>]",
"more_threads_last_month": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多上个月的主题</1>]",
"more_threads_last_year": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多去年的主题</1>]"
"more_threads_last_year": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多去年的主题</1>]",
"iframe_challenge_confirm": "{{board}} 想要打开 {{site}}。\n\n对于 {{publicationType}}{{excerpt}}"
}
@@ -31,6 +31,9 @@ vi.mock('react-i18next', () => ({
if (key === 'challenge_counter') {
return `${options?.index}/${options?.total}`;
}
if (key === 'iframe_challenge_confirm') {
return `${options?.board} wants to open ${options?.site}.\n\nFor ${options?.publicationType}: ${options?.excerpt}`;
}
return key;
},
}),
@@ -88,8 +91,10 @@ vi.mock('@use-gesture/react', () => ({
}));
let alertSpy: ReturnType<typeof vi.spyOn>;
let confirmSpy: ReturnType<typeof vi.spyOn>;
let container: HTMLDivElement;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let nextStoredChallengeId = 1;
let postMessageMock: ReturnType<typeof vi.fn>;
let root: Root;
@@ -106,13 +111,16 @@ const createPublication = (): Record<string, any> => ({
const createStoredChallenge = (challenge: any, publication = createPublication(), publicationTarget?: Record<string, unknown>) => ({
challenge: [{ challenges: Array.isArray(challenge) ? challenge : [challenge] }, publication, publicationTarget],
id: 1,
id: nextStoredChallengeId++,
});
const renderModal = async () => {
await act(async () => {
root.render(createElement(ChallengeModal));
});
await act(async () => {
await new Promise<void>((resolve) => queueMicrotask(resolve));
});
};
const dispatchInput = async (element: HTMLInputElement, value: string) => {
@@ -155,6 +163,7 @@ describe('ChallengeModal', () => {
testState.theme = 'dark';
testState.votePreview = 'upvote';
alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined);
confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true);
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
postMessageMock = vi.fn();
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
@@ -172,6 +181,7 @@ describe('ChallengeModal', () => {
act(() => root.unmount());
container.remove();
alertSpy.mockRestore();
confirmSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
@@ -264,9 +274,16 @@ describe('ChallengeModal', () => {
];
await renderModal();
expect(container.textContent).toContain('mu wants to open mintpass.org');
await clickButton('Open');
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open mintpass.org.\n\nFor post: Subject');
expect(container.textContent).toContain('mintpass.org');
expect(container.textContent).toContain('Done');
expect(container.textContent).not.toContain('Challenge for post');
expect(container.textContent).not.toContain('Open');
expect(container.textContent).not.toContain('Cancel');
expect(container.textContent).not.toContain('Alice');
expect(container.textContent).not.toContain('Subject');
expect(container.textContent).not.toContain('Publication content');
expect(container.textContent).not.toContain('mu wants to open');
const iframe = container.querySelector('iframe');
expect(iframe).not.toBeNull();
@@ -304,9 +321,8 @@ describe('ChallengeModal', () => {
];
await renderModal();
expect(container.textContent).toContain('mu wants to open localhost:3000');
await clickButton('Open');
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open localhost:3000.\n\nFor post: Subject');
const iframe = container.querySelector('iframe');
expect(iframe).not.toBeNull();
@@ -339,7 +355,6 @@ describe('ChallengeModal', () => {
];
await renderModal();
await clickButton('Open');
await act(async () => {
window.dispatchEvent(
@@ -371,7 +386,6 @@ describe('ChallengeModal', () => {
];
await renderModal();
await clickButton('Open');
await act(async () => {
window.dispatchEvent(
@@ -408,8 +422,47 @@ describe('ChallengeModal', () => {
];
await renderModal();
expect(container.textContent).toContain(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000`);
expect(container.textContent).not.toContain(`${longCommunityAddress} wants to open localhost:3000`);
expect(confirmSpy).toHaveBeenCalledWith(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000.\n\nFor post: Subject`);
expect(confirmSpy.mock.calls[0]?.[0]).not.toContain(longCommunityAddress);
});
it('uses reply content in the iframe confirmation excerpt', async () => {
const publication = {
...createPublication(),
content: '>>17\nA reply body with enough context',
title: '',
};
testState.publicationType = 'reply';
testState.challenges = [
createStoredChallenge(
{
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
type: 'url/iframe',
},
publication,
),
];
await renderModal();
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open spamblocker.bitsocial.net.\n\nFor reply: >>17 A reply body with enough context');
});
it('abandons iframe challenges when the external confirmation is canceled', async () => {
confirmSpy.mockReturnValueOnce(false);
testState.challenges = [
createStoredChallenge({
challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123',
type: 'url/iframe',
}),
];
await renderModal();
expect(confirmSpy).toHaveBeenCalledWith('mu wants to open spamblocker.bitsocial.net.\n\nFor post: Subject');
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
expect(container.querySelector('iframe')).toBeNull();
});
it('alerts when iframe challenges need a signer address and the account is missing one', async () => {
@@ -422,9 +475,10 @@ describe('ChallengeModal', () => {
];
await renderModal();
await clickButton('Open');
expect(alertSpy).toHaveBeenCalledWith('Error: Unable to load challenge without your address. Please sign in and try again.');
expect(confirmSpy).not.toHaveBeenCalled();
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
expect(container.querySelector('iframe')).toBeNull();
});
@@ -437,9 +491,9 @@ describe('ChallengeModal', () => {
];
await renderModal();
await clickButton('Open');
expect(alertSpy).toHaveBeenCalledWith('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
expect(confirmSpy).not.toHaveBeenCalled();
expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce();
await act(async () => {
@@ -106,13 +106,6 @@
overflow: hidden;
}
.iframeChallengeWarning {
text-align: center;
padding: 10px;
word-break: break-word;
overflow-wrap: break-word;
}
.iframeWrapper {
width: 500px;
height: 500px;
@@ -1,7 +1,7 @@
import { useRef, useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Challenge as ChallengeType, useAccount, useComment } from '@bitsocial/bitsocial-react-hooks';
import { getPublicationPreview, getPublicationType, getVotePreview } from '../../lib/utils/challenge-utils';
import { getPublicationPreview, getPublicationType, getVotePreview, type ChallengePublication } from '../../lib/utils/challenge-utils';
import useIsMobile from '../../hooks/use-is-mobile';
import useChallengesStore from '../../stores/use-challenges-store';
import useTheme from '../../hooks/use-theme';
@@ -18,10 +18,33 @@ const useParentAddress = (parentCid?: string) => {
interface ChallengeProps {
challenge: ChallengeType;
challengeId: number;
closeModal: () => void;
abandonModal: () => void;
}
const MAX_IFRAME_CONFIRM_EXCERPT_LENGTH = 80;
const getTrimmedExcerpt = (value: unknown) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '');
const shortenConfirmExcerpt = (excerpt: string) =>
excerpt.length > MAX_IFRAME_CONFIRM_EXCERPT_LENGTH ? `${excerpt.slice(0, MAX_IFRAME_CONFIRM_EXCERPT_LENGTH).trimEnd()}...` : excerpt;
const getIframeConfirmExcerpt = (publication: ChallengePublication | undefined, publicationTarget: ChallengePublication | undefined, publicationType?: string) => {
const title = getTrimmedExcerpt(publication?.title);
const content = getTrimmedExcerpt(publication?.content);
const link = getTrimmedExcerpt(publication?.link);
const excerpt =
publicationType === 'vote' ? getTrimmedExcerpt(getPublicationPreview(publicationTarget)) : publicationType === 'reply' ? content || link : title || content || link;
return shortenConfirmExcerpt(excerpt || getTrimmedExcerpt(getPublicationPreview(publication)));
};
const getDisplayCommunityAddress = (shortCommunityAddress?: string, communityAddress?: string) =>
shortCommunityAddress || (communityAddress ? getShortAddress(communityAddress) : '') || communityAddress || '';
const iframeChallengeConfirmDecisions = new Map<string, 'accepted' | 'rejected'>();
const TextChallenge = ({ challenge }: { challenge: string }) => <div className={styles.challengeMedia}>{challenge}</div>;
const MAX_IMAGE_CHALLENGE_BASE64_LENGTH = 2_000_000;
@@ -36,6 +59,36 @@ const ImageChallenge = ({ challenge }: { challenge: string }) =>
const isLocalIframeHostname = (hostname: string) => hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname.endsWith('.localhost');
type ValidatedIframeUrl = { status: 'valid'; finalUrl: string; origin: string } | { status: 'invalid'; error: unknown } | { status: 'unsupported' };
const validateIframeChallengeUrl = (iframeUrl: string, theme: string): ValidatedIframeUrl => {
let validatedUrl: URL;
try {
validatedUrl = new URL(iframeUrl);
} catch (error) {
return { status: 'invalid', error };
}
const isHttps = validatedUrl.protocol === 'https:';
const isLocalHttp = validatedUrl.protocol === 'http:' && isLocalIframeHostname(validatedUrl.hostname);
if (!isHttps && !isLocalHttp) {
return { status: 'unsupported' };
}
validatedUrl.pathname = validatedUrl.pathname.replace(/\/{2,}/g, '/');
validatedUrl.searchParams.set('theme', theme);
return { status: 'valid', finalUrl: validatedUrl.toString(), origin: validatedUrl.origin };
};
const postThemeToIframe = (iframe: HTMLIFrameElement | null, iframeOrigin: string, theme: string) => {
if (!iframe || !iframeOrigin) return;
try {
iframe.contentWindow?.postMessage({ type: 'plebbit-theme', theme, source: 'plebbit-5chan' }, iframeOrigin);
} catch (error) {
console.warn('Could not send theme to iframe:', error);
}
};
const getReadableIframeUrl = (challengeUrl: string) => {
try {
const url = new URL(challengeUrl);
@@ -57,35 +110,44 @@ const getIframeSessionId = (challengeUrl: string) => {
interface IframeChallengeProps {
challenge: string;
shortCommunityAddress?: string;
communityAddress?: string;
readableUrl: string;
confirmKey: string;
confirmMessage: string;
onCancel: () => void;
onDone: () => void;
onAutoComplete: (challengeAnswers: string[]) => void;
publicationDetails: React.ReactNode;
onReady: () => void;
}
const IframeChallenge = ({
challenge,
shortCommunityAddress,
communityAddress,
readableUrl,
onCancel,
onDone,
onAutoComplete,
publicationDetails,
}: IframeChallengeProps) => {
const IframeChallenge = ({ challenge, confirmKey, confirmMessage, onCancel, onDone, onAutoComplete, onReady }: IframeChallengeProps) => {
const account = useAccount();
const [theme] = useTheme();
const [showIframeConfirmation, setShowIframeConfirmation] = useState(true);
const [iframeUrlState, setIframeUrl] = useState('');
const [iframeOrigin, setIframeOrigin] = useState('');
const iframeRef = useRef<HTMLIFrameElement>(null);
const attemptedLoadRef = useRef(false);
const mountedRef = useRef(false);
const handledAutoCompleteRef = useRef(false);
const displayCommunityAddress = shortCommunityAddress || (communityAddress ? getShortAddress(communityAddress) : '') || communityAddress || 'unknown board';
const expectedSessionId = getIframeSessionId(challenge);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
const openValidatedIframe = useCallback(
(validatedUrl: { finalUrl: string; origin: string }) => {
queueMicrotask(() => {
if (!mountedRef.current) return;
setIframeUrl(validatedUrl.finalUrl);
setIframeOrigin(validatedUrl.origin);
onReady();
});
},
[onReady],
);
const handleLoadIframe = useCallback(() => {
const iframeUrl = challenge;
if (!iframeUrl) return;
@@ -95,41 +157,52 @@ const IframeChallenge = ({
if (requiresUserAddress && !rawUserAddress) {
alert('Error: Unable to load challenge without your address. Please sign in and try again.');
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
onCancel();
return;
}
const encodedAddress = rawUserAddress ? encodeURIComponent(rawUserAddress) : undefined;
const replacedUrl = requiresUserAddress && encodedAddress ? iframeUrl.replace(/\{userAddress\}/g, encodedAddress) : iframeUrl;
try {
const validatedUrl = new URL(replacedUrl);
const isHttps = validatedUrl.protocol === 'https:';
const isLocalHttp = validatedUrl.protocol === 'http:' && isLocalIframeHostname(validatedUrl.hostname);
if (!isHttps && !isLocalHttp) {
alert('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
const validatedUrl = validateIframeChallengeUrl(replacedUrl, theme);
if (validatedUrl.status === 'unsupported') {
alert('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported');
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
onCancel();
return;
}
if (validatedUrl.status === 'invalid') {
console.error('Invalid iframe challenge URL', { error: validatedUrl.error });
alert('Error: Invalid URL for authentication challenge');
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
onCancel();
return;
}
const decision = iframeChallengeConfirmDecisions.get(confirmKey);
if (decision === 'rejected') {
onCancel();
return;
}
if (decision !== 'accepted') {
if (!window.confirm(confirmMessage)) {
iframeChallengeConfirmDecisions.set(confirmKey, 'rejected');
onCancel();
return;
}
validatedUrl.pathname = validatedUrl.pathname.replace(/\/{2,}/g, '/');
validatedUrl.searchParams.set('theme', theme);
const finalUrl = validatedUrl.toString();
setIframeUrl(finalUrl);
setIframeOrigin(validatedUrl.origin);
setShowIframeConfirmation(false);
} catch (error) {
console.error('Invalid iframe challenge URL', { error });
alert('Error: Invalid URL for authentication challenge');
onCancel();
iframeChallengeConfirmDecisions.set(confirmKey, 'accepted');
}
}, [account, challenge, onCancel, theme]);
openValidatedIframe(validatedUrl);
}, [account, challenge, confirmKey, confirmMessage, onCancel, openValidatedIframe, theme]);
useEffect(() => {
if (attemptedLoadRef.current) return;
attemptedLoadRef.current = true;
handleLoadIframe();
}, [handleLoadIframe]);
const sendThemeToIframe = useCallback(() => {
if (!iframeRef.current || !iframeOrigin) return;
try {
iframeRef.current.contentWindow?.postMessage({ type: 'plebbit-theme', theme, source: 'plebbit-5chan' }, iframeOrigin);
} catch (error) {
console.warn('Could not send theme to iframe:', error);
}
postThemeToIframe(iframeRef.current, iframeOrigin, theme);
}, [iframeOrigin, theme]);
const handleIframeLoad = () => {
@@ -137,13 +210,13 @@ const IframeChallenge = ({
};
useEffect(() => {
if (iframeRef.current && iframeUrlState && iframeOrigin && !showIframeConfirmation) {
if (iframeRef.current && iframeUrlState && iframeOrigin) {
sendThemeToIframe();
}
}, [iframeOrigin, iframeUrlState, sendThemeToIframe, showIframeConfirmation]);
}, [iframeOrigin, iframeUrlState, sendThemeToIframe]);
useEffect(() => {
if (showIframeConfirmation || !iframeOrigin || !expectedSessionId) {
if (!iframeOrigin || !expectedSessionId) {
handledAutoCompleteRef.current = false;
return;
}
@@ -164,25 +237,10 @@ const IframeChallenge = ({
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [expectedSessionId, iframeOrigin, onAutoComplete, showIframeConfirmation]);
}, [expectedSessionId, iframeOrigin, onAutoComplete]);
if (showIframeConfirmation) {
return (
<>
{publicationDetails}
<div className={styles.challengeMediaWrapper}>
<div className={`${styles.challengeMedia} ${styles.iframeChallengeWarning}`}>
{displayCommunityAddress} wants to open {readableUrl || 'an external site'}
</div>
</div>
<div className={`${styles.challengeFooter} ${styles.iframeFooter}`}>
<span className={styles.buttons}>
<button onClick={handleLoadIframe}>Open</button>
<button onClick={onCancel}>Cancel</button>
</span>
</div>
</>
);
if (!iframeUrlState) {
return null;
}
return (
@@ -206,7 +264,7 @@ const IframeChallenge = ({
);
};
const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: ChallengeProps) => {
const { t } = useTranslation();
const challenges = challenge?.[0]?.challenges;
@@ -224,6 +282,7 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
const [currentChallengeIndex, setCurrentChallengeIndex] = useState(0);
const [answers, setAnswers] = useState<string[]>([]);
const [readyIframeChallengeKey, setReadyIframeChallengeKey] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const nodeRef = useRef<HTMLDivElement>(null);
@@ -235,9 +294,11 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
}));
const currentChallenge = challenges?.[currentChallengeIndex];
const iframeChallengeKey = `${challengeId}:${currentChallengeIndex}:${currentChallenge?.challenge ?? ''}`;
const isTextChallenge = currentChallenge?.type === 'text/plain';
const isImageChallenge = currentChallenge?.type === 'image/png';
const isIframeChallenge = currentChallenge?.type === 'url/iframe';
const isIframePending = isIframeChallenge && readyIframeChallengeKey !== iframeChallengeKey;
useEffect(() => {
inputRef.current?.focus();
@@ -294,6 +355,10 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
[closeModal, publication],
);
const onIframeReady = useCallback(() => {
setReadyIframeChallengeKey(iframeChallengeKey);
}, [iframeChallengeKey]);
const onEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter') return;
if (!isValidAnswer(currentChallengeIndex)) return;
@@ -340,6 +405,15 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
const mobileX = isIframeVisible ? 5 : window.innerWidth / 2 - 150;
const mobileY = isIframeVisible ? Math.max(10, (window.innerHeight - 600) / 2) : window.innerHeight / 2 - 200;
const displayCommunityAddress = getDisplayCommunityAddress(shortCommunityAddress, communityAddress) || t('board');
const iframeConfirmMessage = t('iframe_challenge_confirm', {
board: displayCommunityAddress,
excerpt: getIframeConfirmExcerpt(publication, publicationTarget, publicationType) || t('none'),
interpolation: { escapeValue: false },
publicationType: publicationType ? t(publicationType) : t('post'),
site: readableUrl || t('webpage'),
});
const challengeTitle = isIframeChallenge ? readableUrl || t('iframe') : `Challenge for ${publicationType}`;
const publicationDetails = (
<>
@@ -374,11 +448,12 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
style={{
x: isMobile ? mobileX : x.to((value) => Math.round(value)),
y: isMobile ? mobileY : y.to((value) => Math.round(value)),
display: isIframePending ? 'none' : undefined,
touchAction: 'none',
}}
>
<div id='challenge-modal-title' className={`challengeHandle ${styles.title}`} {...(!isMobile ? bind() : {})}>
Challenge for {publicationType}
{challengeTitle}
<button type='button' className={styles.closeIcon} onClick={abandonModal} title='close' aria-label={t('close')} />
</div>
<div className={styles.publication}>
@@ -386,13 +461,12 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => {
<IframeChallenge
key={currentChallengeIndex}
challenge={currentChallenge?.challenge ?? ''}
shortCommunityAddress={shortCommunityAddress}
communityAddress={communityAddress}
readableUrl={readableUrl}
confirmKey={iframeChallengeKey}
confirmMessage={iframeConfirmMessage}
onCancel={abandonModal}
onDone={onIframeDone}
onAutoComplete={onIframeAutoComplete}
publicationDetails={publicationDetails}
onReady={onIframeReady}
/>
) : (
<>
@@ -450,7 +524,7 @@ const ChallengeModal = () => {
const challenge = current?.challenge;
const challengeId = current?.id ?? 0;
return isOpen && challenge ? <Challenge key={challengeId} challenge={challenge} closeModal={closeModal} abandonModal={abandonModal} /> : null;
return isOpen && challenge ? <Challenge key={challengeId} challenge={challenge} challengeId={challengeId} closeModal={closeModal} abandonModal={abandonModal} /> : null;
};
export default ChallengeModal;