diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index 852b249e..da8bfa16 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الأسبوع الماضي]", "more_threads_last_month": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الشهر الماضي]", "more_threads_last_year": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من السنة الماضية]", - "iframe_challenge_confirm": "{{board}} يريد فتح {{site}}.\n\nبالنسبة إلى {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "غير متاح من هذه الشبكة. جرّب شبكة أخرى أو VPN." + "iframe_challenge_confirm": "{{board}} يريد فتح {{site}}.", + "media_hosting_provider_unavailable": "غير متاح من هذه الشبكة. جرّب شبكة أخرى أو VPN.", + "board_link_permissions": "أذونات روابط اللوحات", + "trusted_board_links_intro": "يمكن للوحات فتح هذه المواقع بدون سؤال.", + "trusted_board_links_empty": "لا توجد روابط لوحات موثوقة.", + "trusted_board_link_until_revoked": "حتى يتم الإلغاء", + "trusted_board_link_checkbox": "اسمح دائمًا للوحات بفتح هذا الموقع الإلكتروني", + "revoke": "إلغاء" } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index 890e1fe2..9b82c96b 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত সপ্তাহের আরও থ্রেড দেখান]", "more_threads_last_month": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত মাসের আরও থ্রেড দেখান]", "more_threads_last_year": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত বছরের আরও থ্রেড দেখান]", - "iframe_challenge_confirm": "{{board}} {{site}} খুলতে চায়।\n\n{{publicationType}}-এর জন্য: {{excerpt}}", - "media_hosting_provider_unavailable": "এই নেটওয়ার্ক থেকে উপলভ্য নয়। অন্য নেটওয়ার্ক বা একটি VPN চেষ্টা করুন।" + "iframe_challenge_confirm": "{{board}} {{site}} খুলতে চায়।", + "media_hosting_provider_unavailable": "এই নেটওয়ার্ক থেকে উপলভ্য নয়। অন্য নেটওয়ার্ক বা একটি VPN চেষ্টা করুন।", + "board_link_permissions": "বোর্ড লিংক অনুমতি", + "trusted_board_links_intro": "বোর্ডগুলো জিজ্ঞাসা না করেই এই সাইটগুলো খুলতে পারে।", + "trusted_board_links_empty": "কোনো বিশ্বস্ত বোর্ড লিংক নেই।", + "trusted_board_link_until_revoked": "প্রত্যাহার না করা পর্যন্ত", + "trusted_board_link_checkbox": "বোর্ডগুলোকে সবসময় এই ওয়েবসাইট খুলতে দিন", + "revoke": "প্রত্যাহার করুন" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 950ff808..02fdbdcb 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} vláken od {{currentTimeFilterName}} — [<1>Ukázat více vláken z minulého týdne]", "more_threads_last_month": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého měsíce]", "more_threads_last_year": "{{count}} příspěvků od {{currentTimeFilterName}} — [<1>Zobrazit více příspěvků z minulého roku]", - "iframe_challenge_confirm": "{{board}} chce otevřít {{site}}.\n\nPro {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Z této sítě není k dispozici. Zkuste jinou síť nebo VPN." + "iframe_challenge_confirm": "{{board}} chce otevřít {{site}}.", + "media_hosting_provider_unavailable": "Z této sítě není k dispozici. Zkuste jinou síť nebo VPN.", + "board_link_permissions": "Oprávnění odkazů desek", + "trusted_board_links_intro": "Desky mohou tyto weby otevírat bez dotazu.", + "trusted_board_links_empty": "Žádné důvěryhodné odkazy desek.", + "trusted_board_link_until_revoked": "dokud nebude odvoláno", + "trusted_board_link_checkbox": "Vždy povolit deskám otevírat tento web", + "revoke": "Odvolat" } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index 7810ad7f..19ec10ff 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste uge]", "more_threads_last_month": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste måned]", "more_threads_last_year": "{{count}} tråde siden {{currentTimeFilterName}} — [<1>Vis flere tråde fra sidste år]", - "iframe_challenge_confirm": "{{board}} vil åbne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Ikke tilgængelig fra dette netværk. Prøv et andet netværk eller en VPN." + "iframe_challenge_confirm": "{{board}} vil åbne {{site}}.", + "media_hosting_provider_unavailable": "Ikke tilgængelig fra dette netværk. Prøv et andet netværk eller en VPN.", + "board_link_permissions": "Tilladelser til tavlelinks", + "trusted_board_links_intro": "Tavler kan åbne disse websteder uden at spørge.", + "trusted_board_links_empty": "Ingen betroede tavlelinks.", + "trusted_board_link_until_revoked": "indtil tilbagekaldt", + "trusted_board_link_checkbox": "Tillad altid tavler at åbne dette websted", + "revoke": "Tilbagekald" } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 3592395c..0a735125 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads aus der letzten Woche anzeigen]", "more_threads_last_month": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Monat anzeigen]", "more_threads_last_year": "{{count}} Threads seit {{currentTimeFilterName}} — [<1>Mehr Threads vom letzten Jahr anzeigen]", - "iframe_challenge_confirm": "{{board}} möchte {{site}} öffnen.\n\nFür {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Von diesem Netzwerk aus nicht verfügbar. Versuche ein anderes Netzwerk oder ein VPN." + "iframe_challenge_confirm": "{{board}} möchte {{site}} öffnen.", + "media_hosting_provider_unavailable": "Von diesem Netzwerk aus nicht verfügbar. Versuche ein anderes Netzwerk oder ein VPN.", + "board_link_permissions": "Berechtigungen für Board-Links", + "trusted_board_links_intro": "Boards können diese Websites ohne Nachfrage öffnen.", + "trusted_board_links_empty": "Keine vertrauenswürdigen Board-Links.", + "trusted_board_link_until_revoked": "bis zum Widerruf", + "trusted_board_link_checkbox": "Boards immer erlauben, diese Website zu öffnen", + "revoke": "Widerrufen" } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index 0680a066..6ab17369 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} θέματα από {{currentTimeFilterName}} — [<1>Εμφάνιση περισσότερων θεμάτων από την περασμένη εβδομάδα]", "more_threads_last_month": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από τον τελευταίο μήνα]", "more_threads_last_year": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από το περασμένο έτος]", - "iframe_challenge_confirm": "Το {{board}} θέλει να ανοίξει το {{site}}.\n\nΓια {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Δεν είναι διαθέσιμο από αυτό το δίκτυο. Δοκιμάστε άλλο δίκτυο ή VPN." + "iframe_challenge_confirm": "Το {{board}} θέλει να ανοίξει το {{site}}.", + "media_hosting_provider_unavailable": "Δεν είναι διαθέσιμο από αυτό το δίκτυο. Δοκιμάστε άλλο δίκτυο ή VPN.", + "board_link_permissions": "Δικαιώματα συνδέσμων πινάκων", + "trusted_board_links_intro": "Οι πίνακες μπορούν να ανοίγουν αυτούς τους ιστότοπους χωρίς ερώτηση.", + "trusted_board_links_empty": "Δεν υπάρχουν αξιόπιστοι σύνδεσμοι πινάκων.", + "trusted_board_link_until_revoked": "μέχρι να ανακληθεί", + "trusted_board_link_checkbox": "Να επιτρέπεται πάντα στους πίνακες να ανοίγουν αυτόν τον ιστότοπο", + "revoke": "Ανάκληση" } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 038d62e6..a220ce81 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Week]", "more_threads_last_month": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Month]", "more_threads_last_year": "{{count}} threads since {{currentTimeFilterName}} — [<1>Show More Threads from Last Year]", - "iframe_challenge_confirm": "{{board}} wants to open {{site}}.\n\nFor {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN." + "iframe_challenge_confirm": "{{board}} wants to open {{site}}.", + "media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN.", + "board_link_permissions": "Board link permissions", + "trusted_board_links_intro": "Boards can open these sites without asking.", + "trusted_board_links_empty": "No trusted board links.", + "trusted_board_link_until_revoked": "until revoked", + "trusted_board_link_checkbox": "Always allow boards to open this website", + "revoke": "Revoke" } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 7492281b..9661094f 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Mostrar más hilos de la semana pasada]", "more_threads_last_month": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del mes pasado]", "more_threads_last_year": "{{count}} hilos desde {{currentTimeFilterName}} — [<1>Ver más hilos del año pasado]", - "iframe_challenge_confirm": "{{board}} quiere abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "No disponible desde esta red. Prueba con otra red o una VPN." + "iframe_challenge_confirm": "{{board}} quiere abrir {{site}}.", + "media_hosting_provider_unavailable": "No disponible desde esta red. Prueba con otra red o una VPN.", + "board_link_permissions": "Permisos de enlaces de tableros", + "trusted_board_links_intro": "Los tableros pueden abrir estos sitios sin preguntar.", + "trusted_board_links_empty": "No hay enlaces de tableros de confianza.", + "trusted_board_link_until_revoked": "hasta que se revoque", + "trusted_board_link_checkbox": "Permitir siempre que los tableros abran este sitio web", + "revoke": "Revocar" } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index 3d6604aa..8d75fb45 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} موضوع از {{currentTimeFilterName}} — [<1>نمایش موضوعات بیشتر از هفته گذشته]", "more_threads_last_month": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های ماه گذشته]", "more_threads_last_year": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پست‌های سال گذشته]", - "iframe_challenge_confirm": "{{board}} می‌خواهد {{site}} را باز کند.\n\nبرای {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "از این شبکه در دسترس نیست. شبکه دیگری یا یک VPN را امتحان کنید." + "iframe_challenge_confirm": "{{board}} می‌خواهد {{site}} را باز کند.", + "media_hosting_provider_unavailable": "از این شبکه در دسترس نیست. شبکه دیگری یا یک VPN را امتحان کنید.", + "board_link_permissions": "مجوزهای پیوندهای برد", + "trusted_board_links_intro": "بردها می‌توانند این سایت‌ها را بدون پرسش باز کنند.", + "trusted_board_links_empty": "هیچ پیوند برد مورد اعتمادی وجود ندارد.", + "trusted_board_link_until_revoked": "تا زمان لغو", + "trusted_board_link_checkbox": "همیشه به بردها اجازه دهید این وب‌سایت را باز کنند", + "revoke": "لغو" } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index ded1f63b..56d529b1 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää ketjuja viime viikolta]", "more_threads_last_month": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime kuun ketjuja]", "more_threads_last_year": "{{count}} ketjua sitten {{currentTimeFilterName}} — [<1>Näytä lisää viime vuoden ketjuja]", - "iframe_challenge_confirm": "{{board}} haluaa avata sivuston {{site}}.\n\nKohteelle {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Ei saatavilla tästä verkosta. Kokeile toista verkkoa tai VPN:ää." + "iframe_challenge_confirm": "{{board}} haluaa avata sivuston {{site}}.", + "media_hosting_provider_unavailable": "Ei saatavilla tästä verkosta. Kokeile toista verkkoa tai VPN:ää.", + "board_link_permissions": "Taululinkkien käyttöoikeudet", + "trusted_board_links_intro": "Taulut voivat avata nämä sivustot kysymättä.", + "trusted_board_links_empty": "Ei luotettuja taululinkkejä.", + "trusted_board_link_until_revoked": "kunnes perutaan", + "trusted_board_link_checkbox": "Salli taulujen aina avata tämä verkkosivusto", + "revoke": "Peruuta" } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 9023d6ff..13ea0d72 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} mga thread mula sa {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula sa nakaraang linggo]", "more_threads_last_month": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang buwan]", "more_threads_last_year": "{{count}} mga thread mula {{currentTimeFilterName}} — [<1>Ipakita ang higit pang mga thread mula noong nakaraang taon]", - "iframe_challenge_confirm": "Nais buksan ng {{board}} ang {{site}}.\n\nPara sa {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Hindi available mula sa network na ito. Subukan ang ibang network o VPN." + "iframe_challenge_confirm": "Nais buksan ng {{board}} ang {{site}}.", + "media_hosting_provider_unavailable": "Hindi available mula sa network na ito. Subukan ang ibang network o VPN.", + "board_link_permissions": "Mga pahintulot sa link ng board", + "trusted_board_links_intro": "Maaaring buksan ng mga board ang mga site na ito nang hindi nagtatanong.", + "trusted_board_links_empty": "Walang pinagkakatiwalaang link ng board.", + "trusted_board_link_until_revoked": "hanggang bawiin", + "trusted_board_link_checkbox": "Palaging payagan ang mga board na buksan ang website na ito", + "revoke": "Bawiin" } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 47167153..0f291214 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de la semaine dernière]", "more_threads_last_month": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils du mois dernier]", "more_threads_last_year": "{{count}} fils depuis {{currentTimeFilterName}} — [<1>Afficher plus de fils de l'année dernière]", - "iframe_challenge_confirm": "{{board}} veut ouvrir {{site}}.\n\nPour {{publicationType}} : {{excerpt}}", - "media_hosting_provider_unavailable": "Indisponible depuis ce réseau. Essayez un autre réseau ou un VPN." + "iframe_challenge_confirm": "{{board}} veut ouvrir {{site}}.", + "media_hosting_provider_unavailable": "Indisponible depuis ce réseau. Essayez un autre réseau ou un VPN.", + "board_link_permissions": "Autorisations des liens de boards", + "trusted_board_links_intro": "Les boards peuvent ouvrir ces sites sans demander.", + "trusted_board_links_empty": "Aucun lien de board approuvé.", + "trusted_board_link_until_revoked": "jusqu'à révocation", + "trusted_board_link_checkbox": "Toujours autoriser les boards à ouvrir ce site web", + "revoke": "Révoquer" } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 9763d26b..929b204b 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} אשכולות מאז {{currentTimeFilterName}} — [<1>הצג עוד אשכולות מהשבוע שעבר]", "more_threads_last_month": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהחודש שעבר]", "more_threads_last_year": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהשנה שעברה]", - "iframe_challenge_confirm": "{{board}} רוצה לפתוח את {{site}}.\n\nעבור {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "לא זמין מהרשת הזו. נסו רשת אחרת או VPN." + "iframe_challenge_confirm": "{{board}} רוצה לפתוח את {{site}}.", + "media_hosting_provider_unavailable": "לא זמין מהרשת הזו. נסו רשת אחרת או VPN.", + "board_link_permissions": "הרשאות קישורי לוחות", + "trusted_board_links_intro": "לוחות יכולים לפתוח את האתרים האלה בלי לשאול.", + "trusted_board_links_empty": "אין קישורי לוחות מהימנים.", + "trusted_board_link_until_revoked": "עד לביטול", + "trusted_board_link_checkbox": "לאפשר תמיד ללוחות לפתוח את האתר הזה", + "revoke": "בטל" } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index d155dc63..d13eed7b 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} थ्रेड {{currentTimeFilterName}} से — [<1>पिछले सप्ताह के और थ्रेड दिखाएं]", "more_threads_last_month": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>अगले महीने के अधिक थ्रेड्स दिखाएं]", "more_threads_last_year": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>पिछले साल के अधिक थ्रेड्स दिखाएं]", - "iframe_challenge_confirm": "{{board}} {{site}} खोलना चाहता है।\n\n{{publicationType}} के लिए: {{excerpt}}", - "media_hosting_provider_unavailable": "इस नेटवर्क से उपलब्ध नहीं है। कोई दूसरा नेटवर्क या VPN आज़माएँ।" + "iframe_challenge_confirm": "{{board}} {{site}} खोलना चाहता है।", + "media_hosting_provider_unavailable": "इस नेटवर्क से उपलब्ध नहीं है। कोई दूसरा नेटवर्क या VPN आज़माएँ।", + "board_link_permissions": "बोर्ड लिंक अनुमतियां", + "trusted_board_links_intro": "बोर्ड बिना पूछे इन साइटों को खोल सकते हैं।", + "trusted_board_links_empty": "कोई विश्वसनीय बोर्ड लिंक नहीं।", + "trusted_board_link_until_revoked": "रद्द होने तक", + "trusted_board_link_checkbox": "बोर्ड को हमेशा यह वेबसाइट खोलने दें", + "revoke": "रद्द करें" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index ebbe90fe..445d8264 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} szál a {{currentTimeFilterName}} óta — [<1>Mutass több szálat a múlt héten]", "more_threads_last_month": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt hónapból]", "more_threads_last_year": "{{count}} szál {{currentTimeFilterName}} óta — [<1>Több szál megjelenítése a múlt évből]", - "iframe_challenge_confirm": "{{board}} meg szeretné nyitni a következőt: {{site}}.\n\n{{publicationType}} esetén: {{excerpt}}", - "media_hosting_provider_unavailable": "Erről a hálózatról nem érhető el. Próbálj ki egy másik hálózatot vagy VPN-t." + "iframe_challenge_confirm": "{{board}} meg szeretné nyitni a következőt: {{site}}.", + "media_hosting_provider_unavailable": "Erről a hálózatról nem érhető el. Próbálj ki egy másik hálózatot vagy VPN-t.", + "board_link_permissions": "Táblahivatkozás-engedélyek", + "trusted_board_links_intro": "A táblák kérdés nélkül megnyithatják ezeket a webhelyeket.", + "trusted_board_links_empty": "Nincsenek megbízható táblahivatkozások.", + "trusted_board_link_until_revoked": "visszavonásig", + "trusted_board_link_checkbox": "Mindig engedélyezze a tábláknak ennek a webhelynek a megnyitását", + "revoke": "Visszavonás" } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index 0fd34e9b..7906d134 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Show lebih banyak thread dari minggu lalu]", "more_threads_last_month": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari bulan lalu]", "more_threads_last_year": "{{count}} thread sejak {{currentTimeFilterName}} — [<1>Tampilkan lebih banyak thread dari tahun lalu]", - "iframe_challenge_confirm": "{{board}} ingin membuka {{site}}.\n\nUntuk {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Tidak tersedia dari jaringan ini. Coba jaringan lain atau VPN." + "iframe_challenge_confirm": "{{board}} ingin membuka {{site}}.", + "media_hosting_provider_unavailable": "Tidak tersedia dari jaringan ini. Coba jaringan lain atau VPN.", + "board_link_permissions": "Izin tautan papan", + "trusted_board_links_intro": "Papan dapat membuka situs ini tanpa bertanya.", + "trusted_board_links_empty": "Tidak ada tautan papan tepercaya.", + "trusted_board_link_until_revoked": "hingga dicabut", + "trusted_board_link_checkbox": "Selalu izinkan papan membuka situs web ini", + "revoke": "Cabut" } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index 3063050a..a08e17e8 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra più thread dalla scorsa settimana]", "more_threads_last_month": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread del mese scorso]", "more_threads_last_year": "{{count}} thread da {{currentTimeFilterName}} — [<1>Mostra altri thread dell'anno scorso]", - "iframe_challenge_confirm": "{{board}} vuole aprire {{site}}.\n\nPer {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Non disponibile da questa rete. Prova un'altra rete o una VPN." + "iframe_challenge_confirm": "{{board}} vuole aprire {{site}}.", + "media_hosting_provider_unavailable": "Non disponibile da questa rete. Prova un'altra rete o una VPN.", + "board_link_permissions": "Autorizzazioni dei link delle bacheche", + "trusted_board_links_intro": "Le bacheche possono aprire questi siti senza chiedere.", + "trusted_board_links_empty": "Nessun link di bacheca attendibile.", + "trusted_board_link_until_revoked": "fino alla revoca", + "trusted_board_link_checkbox": "Consenti sempre alle bacheche di aprire questo sito web", + "revoke": "Revoca" } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index a218f114..12dcae59 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先週のスレッドをさらに表示]", "more_threads_last_month": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先月のスレッドをさらに表示]", "more_threads_last_year": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>去年のスレッドをさらに表示]", - "iframe_challenge_confirm": "{{board}}が{{site}}を開こうとしています。\n\n{{publicationType}}の場合: {{excerpt}}", - "media_hosting_provider_unavailable": "このネットワークからは利用できません。別のネットワークかVPNをお試しください。" + "iframe_challenge_confirm": "{{board}}が{{site}}を開こうとしています。", + "media_hosting_provider_unavailable": "このネットワークからは利用できません。別のネットワークかVPNをお試しください。", + "board_link_permissions": "板リンクの権限", + "trusted_board_links_intro": "板は確認なしでこれらのサイトを開けます。", + "trusted_board_links_empty": "信頼済みの板リンクはありません。", + "trusted_board_link_until_revoked": "取り消すまで", + "trusted_board_link_checkbox": "板が常にこのウェブサイトを開くことを許可する", + "revoke": "取り消す" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index d1a9071e..839fca7a 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 주의 더 많은 스레드 보기]", "more_threads_last_month": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 달의 스레드 더 보기]", "more_threads_last_year": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>작년의 스레드 더 보기]", - "iframe_challenge_confirm": "{{board}}에서 {{site}} 열기를 요청했습니다.\n\n{{publicationType}}의 경우: {{excerpt}}", - "media_hosting_provider_unavailable": "이 네트워크에서는 사용할 수 없습니다. 다른 네트워크나 VPN을 시도해 보세요." + "iframe_challenge_confirm": "{{board}}에서 {{site}} 열기를 요청했습니다.", + "media_hosting_provider_unavailable": "이 네트워크에서는 사용할 수 없습니다. 다른 네트워크나 VPN을 시도해 보세요.", + "board_link_permissions": "게시판 링크 권한", + "trusted_board_links_intro": "게시판이 묻지 않고 이 사이트들을 열 수 있습니다.", + "trusted_board_links_empty": "신뢰한 게시판 링크가 없습니다.", + "trusted_board_link_until_revoked": "취소할 때까지", + "trusted_board_link_checkbox": "게시판이 항상 이 웹사이트를 열도록 허용", + "revoke": "취소" } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index 5eead025..c4309c14 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} थ्रेड {{currentTimeFilterName}} पासून — [<1>गेल्या आठवड्यातील अधिक थ्रेड्स पहा]", "more_threads_last_month": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या महिन्यातील आणखी थ्रेड्स दाखवा]", "more_threads_last_year": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या वर्षातील आणखी थ्रेड्स दाखवा]", - "iframe_challenge_confirm": "{{board}} ला {{site}} उघडायचे आहे.\n\n{{publicationType}} साठी: {{excerpt}}", - "media_hosting_provider_unavailable": "या नेटवर्कवरून उपलब्ध नाही. दुसरे नेटवर्क किंवा VPN वापरून पाहा." + "iframe_challenge_confirm": "{{board}} ला {{site}} उघडायचे आहे.", + "media_hosting_provider_unavailable": "या नेटवर्कवरून उपलब्ध नाही. दुसरे नेटवर्क किंवा VPN वापरून पाहा.", + "board_link_permissions": "बोर्ड दुवा परवानग्या", + "trusted_board_links_intro": "बोर्ड विचारल्याशिवाय या साइट्स उघडू शकतात.", + "trusted_board_links_empty": "विश्वसनीय बोर्ड दुवे नाहीत.", + "trusted_board_link_until_revoked": "रद्द करेपर्यंत", + "trusted_board_link_checkbox": "बोर्डना नेहमी ही वेबसाइट उघडण्याची परवानगी द्या", + "revoke": "रद्द करा" } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index e934a8ac..15a3dd13 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Toon meer threads van vorige week]", "more_threads_last_month": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen maand tonen]", "more_threads_last_year": "{{count}} threads sinds {{currentTimeFilterName}} — [<1>Meer threads van afgelopen jaar tonen]", - "iframe_challenge_confirm": "{{board}} wil {{site}} openen.\n\nVoor {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Niet beschikbaar via dit netwerk. Probeer een ander netwerk of een VPN." + "iframe_challenge_confirm": "{{board}} wil {{site}} openen.", + "media_hosting_provider_unavailable": "Niet beschikbaar via dit netwerk. Probeer een ander netwerk of een VPN.", + "board_link_permissions": "Machtigingen voor boardlinks", + "trusted_board_links_intro": "Boards kunnen deze sites openen zonder te vragen.", + "trusted_board_links_empty": "Geen vertrouwde boardlinks.", + "trusted_board_link_until_revoked": "tot intrekking", + "trusted_board_link_checkbox": "Sta boards altijd toe deze website te openen", + "revoke": "Intrekken" } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index 7ff199bf..c94f005b 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige uke]", "more_threads_last_month": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige måned]", "more_threads_last_year": "{{count}} tråder siden {{currentTimeFilterName}} — [<1>Vis flere tråder fra forrige år]", - "iframe_challenge_confirm": "{{board}} vil åpne {{site}}.\n\nFor {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Ikke tilgjengelig fra dette nettverket. Prøv et annet nettverk eller en VPN." + "iframe_challenge_confirm": "{{board}} vil åpne {{site}}.", + "media_hosting_provider_unavailable": "Ikke tilgjengelig fra dette nettverket. Prøv et annet nettverk eller en VPN.", + "board_link_permissions": "Tillatelser for tavlelenker", + "trusted_board_links_intro": "Tavler kan åpne disse nettstedene uten å spørre.", + "trusted_board_links_empty": "Ingen klarerte tavlelenker.", + "trusted_board_link_until_revoked": "til den tilbakekalles", + "trusted_board_link_checkbox": "Tillat alltid tavler å åpne dette nettstedet", + "revoke": "Tilbakekall" } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index 84aee884..9a4e5952 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego tygodnia]", "more_threads_last_month": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego miesiąca]", "more_threads_last_year": "{{count}} wątki od {{currentTimeFilterName}} — [<1>Pokaż więcej wątków z zeszłego roku]", - "iframe_challenge_confirm": "{{board}} chce otworzyć {{site}}.\n\nDla {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Niedostępne z tej sieci. Spróbuj użyć innej sieci lub VPN." + "iframe_challenge_confirm": "{{board}} chce otworzyć {{site}}.", + "media_hosting_provider_unavailable": "Niedostępne z tej sieci. Spróbuj użyć innej sieci lub VPN.", + "board_link_permissions": "Uprawnienia linków tablic", + "trusted_board_links_intro": "Tablice mogą otwierać te strony bez pytania.", + "trusted_board_links_empty": "Brak zaufanych linków tablic.", + "trusted_board_link_until_revoked": "do odwołania", + "trusted_board_link_checkbox": "Zawsze pozwalaj tablicom otwierać tę stronę internetową", + "revoke": "Odwołaj" } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index a16f3cee..7fa29c96 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos da semana passada]", "more_threads_last_month": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do mês passado]", "more_threads_last_year": "{{count}} tópicos desde {{currentTimeFilterName}} — [<1>Mostrar mais tópicos do ano passado]", - "iframe_challenge_confirm": "{{board}} quer abrir {{site}}.\n\nPara {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Indisponível nesta rede. Tente outra rede ou uma VPN." + "iframe_challenge_confirm": "{{board}} quer abrir {{site}}.", + "media_hosting_provider_unavailable": "Indisponível nesta rede. Tente outra rede ou uma VPN.", + "board_link_permissions": "Permissões de links de boards", + "trusted_board_links_intro": "Os boards podem abrir estes sites sem perguntar.", + "trusted_board_links_empty": "Nenhum link de board confiável.", + "trusted_board_link_until_revoked": "até ser revogado", + "trusted_board_link_checkbox": "Sempre permitir que boards abram este site", + "revoke": "Revogar" } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 1ee1e233..9ed00670 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} fire de discuție de la {{currentTimeFilterName}} — [<1>Arată mai multe fire din săptămâna trecută]", "more_threads_last_month": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de luna trecută]", "more_threads_last_year": "{{count}} subiecte din {{currentTimeFilterName}} — [<1>Afișați mai multe subiecte de anul trecut]", - "iframe_challenge_confirm": "{{board}} vrea să deschidă {{site}}.\n\nPentru {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Indisponibil din această rețea. Încearcă altă rețea sau un VPN." + "iframe_challenge_confirm": "{{board}} vrea să deschidă {{site}}.", + "media_hosting_provider_unavailable": "Indisponibil din această rețea. Încearcă altă rețea sau un VPN.", + "board_link_permissions": "Permisiuni pentru linkurile boardurilor", + "trusted_board_links_intro": "Boardurile pot deschide aceste site-uri fără să întrebe.", + "trusted_board_links_empty": "Nu există linkuri de board de încredere.", + "trusted_board_link_until_revoked": "până la revocare", + "trusted_board_link_checkbox": "Permite mereu boardurilor să deschidă acest site web", + "revoke": "Revocă" } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index 98fd2cb8..8737b0a7 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} тредов с {{currentTimeFilterName}} — [<1>Показать больше тредов за прошлую неделю]", "more_threads_last_month": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого месяца]", "more_threads_last_year": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого года]", - "iframe_challenge_confirm": "{{board}} хочет открыть {{site}}.\n\nДля {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Недоступно из этой сети. Попробуйте другую сеть или VPN." + "iframe_challenge_confirm": "{{board}} хочет открыть {{site}}.", + "media_hosting_provider_unavailable": "Недоступно из этой сети. Попробуйте другую сеть или VPN.", + "board_link_permissions": "Разрешения ссылок досок", + "trusted_board_links_intro": "Доски могут открывать эти сайты без запроса.", + "trusted_board_links_empty": "Нет доверенных ссылок досок.", + "trusted_board_link_until_revoked": "до отзыва", + "trusted_board_link_checkbox": "Всегда разрешать доскам открывать этот сайт", + "revoke": "Отозвать" } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 504404aa..afb5f7b6 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga java e kaluar]", "more_threads_last_month": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga muaji i kaluar]", "more_threads_last_year": "{{count}} tema nga {{currentTimeFilterName}} — [<1>Shfaq më shumë tema nga viti i kaluar]", - "iframe_challenge_confirm": "{{board}} dëshiron të hapë {{site}}.\n\nPër {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Nuk disponohet nga ky rrjet. Provoni një rrjet tjetër ose një VPN." + "iframe_challenge_confirm": "{{board}} dëshiron të hapë {{site}}.", + "media_hosting_provider_unavailable": "Nuk disponohet nga ky rrjet. Provoni një rrjet tjetër ose një VPN.", + "board_link_permissions": "Lejet e lidhjeve të bordeve", + "trusted_board_links_intro": "Bordet mund t'i hapin këto faqe pa pyetur.", + "trusted_board_links_empty": "Nuk ka lidhje bordesh të besuara.", + "trusted_board_link_until_revoked": "derisa të revokohet", + "trusted_board_link_checkbox": "Lejo gjithmonë bordet të hapin këtë faqe interneti", + "revoke": "Revoko" } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index 6820d2f5..038705f3 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra veckan]", "more_threads_last_month": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra månaden]", "more_threads_last_year": "{{count}} trådar sedan {{currentTimeFilterName}} — [<1>Visa fler trådar från förra året]", - "iframe_challenge_confirm": "{{board}} vill öppna {{site}}.\n\nFör {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Inte tillgängligt från det här nätverket. Prova ett annat nätverk eller en VPN." + "iframe_challenge_confirm": "{{board}} vill öppna {{site}}.", + "media_hosting_provider_unavailable": "Inte tillgängligt från det här nätverket. Prova ett annat nätverk eller en VPN.", + "board_link_permissions": "Behörigheter för boardlänkar", + "trusted_board_links_intro": "Boards kan öppna dessa webbplatser utan att fråga.", + "trusted_board_links_empty": "Inga betrodda boardlänkar.", + "trusted_board_link_until_revoked": "tills den återkallas", + "trusted_board_link_checkbox": "Tillåt alltid boards att öppna den här webbplatsen", + "revoke": "Återkalla" } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index 74c21691..31b74c98 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత వారానికి మరిన్ని థ్రెడ్‌లు చూపించు]", "more_threads_last_month": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత నెల నుండి మరిన్ని థ్రెడ్‌లను చూపించండి]", "more_threads_last_year": "{{count}} థ్రెడ్‌లు {{currentTimeFilterName}} నుండి — [<1>గత సంవత్సరం నుండి మరిన్ని థ్రెడ్‌లను చూపించండి]", - "iframe_challenge_confirm": "{{board}} {{site}} ను తెరవాలని కోరుతోంది.\n\n{{publicationType}} కోసం: {{excerpt}}", - "media_hosting_provider_unavailable": "ఈ నెట్‌వర్క్ నుండి అందుబాటులో లేదు. మరో నెట్‌వర్క్ లేదా VPN ప్రయత్నించండి." + "iframe_challenge_confirm": "{{board}} {{site}} ను తెరవాలని కోరుతోంది.", + "media_hosting_provider_unavailable": "ఈ నెట్‌వర్క్ నుండి అందుబాటులో లేదు. మరో నెట్‌వర్క్ లేదా VPN ప్రయత్నించండి.", + "board_link_permissions": "బోర్డ్ లింక్ అనుమతులు", + "trusted_board_links_intro": "బోర్డులు అడగకుండా ఈ సైట్లను తెరవగలవు.", + "trusted_board_links_empty": "నమ్మదగిన బోర్డ్ లింకులు లేవు.", + "trusted_board_link_until_revoked": "రద్దు చేసే వరకు", + "trusted_board_link_checkbox": "బోర్డులు ఎల్లప్పుడూ ఈ వెబ్‌సైట్‌ను తెరవడానికి అనుమతించు", + "revoke": "రద్దు చేయి" } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index a5f12020..73df0cce 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้เพิ่มเติมจากสัปดาห์ที่แล้ว]", "more_threads_last_month": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากเดือนที่แล้วเพิ่มเติม]", "more_threads_last_year": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากปีที่แล้วเพิ่มเติม]", - "iframe_challenge_confirm": "{{board}} ต้องการเปิด {{site}}\n\nสำหรับ {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "ไม่สามารถใช้งานได้จากเครือข่ายนี้ ลองใช้เครือข่ายอื่นหรือ VPN" + "iframe_challenge_confirm": "{{board}} ต้องการเปิด {{site}}", + "media_hosting_provider_unavailable": "ไม่สามารถใช้งานได้จากเครือข่ายนี้ ลองใช้เครือข่ายอื่นหรือ VPN", + "board_link_permissions": "สิทธิ์ลิงก์บอร์ด", + "trusted_board_links_intro": "บอร์ดสามารถเปิดไซต์เหล่านี้ได้โดยไม่ต้องถาม", + "trusted_board_links_empty": "ไม่มีลิงก์บอร์ดที่เชื่อถือได้", + "trusted_board_link_until_revoked": "จนกว่าจะเพิกถอน", + "trusted_board_link_checkbox": "อนุญาตให้บอร์ดเปิดเว็บไซต์นี้เสมอ", + "revoke": "เพิกถอน" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index 501a3683..a41b4bcd 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} başlık {{currentTimeFilterName}}’den itibaren — [<1>Geçen haftadan daha fazla başlık göster]", "more_threads_last_month": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen aydan daha fazla konu göster]", "more_threads_last_year": "{{count}} konu {{currentTimeFilterName}} itibariyle — [<1>Geçen yıldan daha fazla konu göster]", - "iframe_challenge_confirm": "{{board}}, {{site}} sitesini açmak istiyor.\n\n{{publicationType}} için: {{excerpt}}", - "media_hosting_provider_unavailable": "Bu ağdan kullanılamıyor. Başka bir ağ veya VPN deneyin." + "iframe_challenge_confirm": "{{board}}, {{site}} sitesini açmak istiyor.", + "media_hosting_provider_unavailable": "Bu ağdan kullanılamıyor. Başka bir ağ veya VPN deneyin.", + "board_link_permissions": "Pano bağlantısı izinleri", + "trusted_board_links_intro": "Panolar bu siteleri sormadan açabilir.", + "trusted_board_links_empty": "Güvenilir pano bağlantısı yok.", + "trusted_board_link_until_revoked": "iptal edilene kadar", + "trusted_board_link_checkbox": "Panoların bu web sitesini her zaman açmasına izin ver", + "revoke": "İptal et" } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index e03109d5..dfaf3953 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого тижня]", "more_threads_last_month": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого місяця]", "more_threads_last_year": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого року]", - "iframe_challenge_confirm": "{{board}} хоче відкрити {{site}}.\n\nДля {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Недоступно з цієї мережі. Спробуйте іншу мережу або VPN." + "iframe_challenge_confirm": "{{board}} хоче відкрити {{site}}.", + "media_hosting_provider_unavailable": "Недоступно з цієї мережі. Спробуйте іншу мережу або VPN.", + "board_link_permissions": "Дозволи для посилань дощок", + "trusted_board_links_intro": "Дошки можуть відкривати ці сайти без запиту.", + "trusted_board_links_empty": "Немає довірених посилань дощок.", + "trusted_board_link_until_revoked": "до відкликання", + "trusted_board_link_checkbox": "Завжди дозволяти дошкам відкривати цей сайт", + "revoke": "Відкликати" } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 8330e111..b2dbccd7 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} تھریڈ {{currentTimeFilterName}} سے — [<1>گزشتہ ہفتے کے مزید تھریڈ دیکھیں]", "more_threads_last_month": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>پچھلے ماہ کے مزید تھریڈز دکھائیں]", "more_threads_last_year": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>گزشتہ سال کے مزید تھریڈز دکھائیں]", - "iframe_challenge_confirm": "{{board}}، {{site}} کھولنا چاہتا ہے۔\n\n{{publicationType}} کے لیے: {{excerpt}}", - "media_hosting_provider_unavailable": "اس نیٹ ورک سے دستیاب نہیں۔ کوئی دوسرا نیٹ ورک یا VPN آزمائیں۔" + "iframe_challenge_confirm": "{{board}}، {{site}} کھولنا چاہتا ہے۔", + "media_hosting_provider_unavailable": "اس نیٹ ورک سے دستیاب نہیں۔ کوئی دوسرا نیٹ ورک یا VPN آزمائیں۔", + "board_link_permissions": "بورڈ لنک اجازتیں", + "trusted_board_links_intro": "بورڈ یہ سائٹس پوچھے بغیر کھول سکتے ہیں۔", + "trusted_board_links_empty": "کوئی قابل اعتماد بورڈ لنکس نہیں۔", + "trusted_board_link_until_revoked": "منسوخ ہونے تک", + "trusted_board_link_checkbox": "بورڈز کو ہمیشہ یہ ویب سائٹ کھولنے کی اجازت دیں", + "revoke": "منسوخ کریں" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index 45c791df..1375707f 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Xem thêm chủ đề từ tuần trước]", "more_threads_last_month": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ tháng trước]", "more_threads_last_year": "{{count}} chủ đề từ {{currentTimeFilterName}} — [<1>Hiển thị thêm các chủ đề từ năm ngoái]", - "iframe_challenge_confirm": "{{board}} muốn mở {{site}}.\n\nDành cho {{publicationType}}: {{excerpt}}", - "media_hosting_provider_unavailable": "Không khả dụng từ mạng này. Hãy thử mạng khác hoặc VPN." + "iframe_challenge_confirm": "{{board}} muốn mở {{site}}.", + "media_hosting_provider_unavailable": "Không khả dụng từ mạng này. Hãy thử mạng khác hoặc VPN.", + "board_link_permissions": "Quyền liên kết bảng", + "trusted_board_links_intro": "Các bảng có thể mở những trang này mà không cần hỏi.", + "trusted_board_links_empty": "Không có liên kết bảng đáng tin cậy.", + "trusted_board_link_until_revoked": "cho đến khi thu hồi", + "trusted_board_link_checkbox": "Luôn cho phép các bảng mở trang web này", + "revoke": "Thu hồi" } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 677531c7..814b5ca6 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -364,6 +364,12 @@ "more_threads_last_week": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多来自上周的主题]", "more_threads_last_month": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多上个月的主题]", "more_threads_last_year": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多去年的主题]", - "iframe_challenge_confirm": "{{board}} 想要打开 {{site}}。\n\n对于 {{publicationType}}:{{excerpt}}", - "media_hosting_provider_unavailable": "此网络无法使用。请尝试其他网络或 VPN。" + "iframe_challenge_confirm": "{{board}} 想要打开 {{site}}。", + "media_hosting_provider_unavailable": "此网络无法使用。请尝试其他网络或 VPN。", + "board_link_permissions": "版块链接权限", + "trusted_board_links_intro": "版块可以不再询问而打开这些站点。", + "trusted_board_links_empty": "没有受信任的版块链接。", + "trusted_board_link_until_revoked": "直到撤销", + "trusted_board_link_checkbox": "始终允许版块打开此网站", + "revoke": "撤销" } diff --git a/src/components/challenge-modal/__tests__/challenge-modal.test.tsx b/src/components/challenge-modal/__tests__/challenge-modal.test.tsx index fb38e68e..d846348a 100644 --- a/src/components/challenge-modal/__tests__/challenge-modal.test.tsx +++ b/src/components/challenge-modal/__tests__/challenge-modal.test.tsx @@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import ChallengeModal from '../challenge-modal'; import getShortAddress from '../../../lib/get-short-address'; +import useTrustedBoardUrlPermissionsStore from '../../../stores/use-trusted-board-url-permissions-store'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; @@ -15,7 +16,6 @@ const testState = vi.hoisted(() => ({ address: '0xabc123', }, } as Record, - capacitorPlatform: 'web', challenges: [] as Array<{ challenge: any; id: number }>, commentsByCid: {} as Record, publicationPreview: 'preview body', @@ -33,7 +33,10 @@ vi.mock('react-i18next', () => ({ 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 `${options?.board} wants to open ${options?.site}.\n\nFor {{publicationType}}: {{excerpt}}`; + } + if (key === 'trusted_board_link_checkbox') { + return 'Always allow boards to open this website'; } return key; }, @@ -45,12 +48,6 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ useComment: ({ commentCid }: { commentCid?: string }) => (commentCid ? testState.commentsByCid[commentCid] : undefined), })); -vi.mock('@capacitor/core', () => ({ - Capacitor: { - getPlatform: () => testState.capacitorPlatform, - }, -})); - vi.mock('../../../lib/utils/challenge-utils', () => ({ getPublicationPreview: () => testState.publicationPreview, getPublicationType: () => testState.publicationType, @@ -146,23 +143,17 @@ const clickButton = async (text: string) => { }); }; -const setNavigatorValue = (key: keyof Navigator, value: unknown) => { - Object.defineProperty(window.navigator, key, { - configurable: true, - value, - }); -}; - describe('ChallengeModal', () => { beforeEach(() => { vi.clearAllMocks(); + localStorage.clear(); + useTrustedBoardUrlPermissionsStore.setState({ trustedOrigins: {} }); testState.abandonCurrentChallengeMock.mockReset().mockResolvedValue(undefined); testState.account = { author: { address: '0xabc123', }, }; - testState.capacitorPlatform = 'web'; testState.challenges = []; testState.commentsByCid = { 'parent-1': { @@ -177,10 +168,6 @@ describe('ChallengeModal', () => { testState.springStartMock.mockReset(); testState.theme = 'dark'; testState.votePreview = 'upvote'; - setNavigatorValue('maxTouchPoints', 0); - setNavigatorValue('platform', 'MacIntel'); - setNavigatorValue('userAgent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125 Safari/537.36'); - setNavigatorValue('vendor', 'Google Inc.'); alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined); confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); @@ -280,7 +267,7 @@ describe('ChallengeModal', () => { expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); }); - it('opens iframe challenges, injects the theme, and completes them', async () => { + it('opens iframe challenges through the custom modal, injects the theme, and completes them', async () => { const publication = createPublication(); testState.challenges = [ createStoredChallenge( @@ -293,16 +280,25 @@ describe('ChallengeModal', () => { ]; await renderModal(); - expect(confirmSpy).toHaveBeenCalledWith('mu wants to open mintpass.org.\n\nFor post: Subject'); + expect(confirmSpy).not.toHaveBeenCalled(); + expect(container.querySelector('#challenge-modal-title')?.textContent).toContain('Challenge for post'); 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).toContain('mu wants to open mintpass.org.'); + expect(container.textContent).not.toContain('For post:'); + expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}'); + expect(container.textContent).toContain('Open'); + expect(container.textContent).toContain('Always allow boards to open this website'); + expect(container.textContent).not.toContain('Always allow boards to open mintpass.org'); 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 publicationFields = Array.from(container.querySelectorAll('input[type="text"]')); + expect(publicationFields.map((input) => input.value)).toEqual(['Alice', 'Subject', 'https://example.com/link']); + expect(publicationFields.every((input) => input.disabled)).toBe(true); + const publicationTextarea = container.querySelector('textarea'); + expect(publicationTextarea?.value).toBe('Publication content'); + expect(publicationTextarea?.disabled).toBe(true); + expect(container.querySelector('iframe')).toBeNull(); + + await clickButton('Open'); const iframe = container.querySelector('iframe'); expect(iframe).not.toBeNull(); @@ -327,6 +323,47 @@ describe('ChallengeModal', () => { expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); }); + it('remembers a trusted iframe origin when the checkbox is selected', async () => { + testState.challenges = [ + createStoredChallenge({ + challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123', + type: 'url/iframe', + }), + ]; + + await renderModal(); + + const checkbox = container.querySelector('input[type="checkbox"]'); + await act(async () => { + checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + await clickButton('Open'); + + expect(useTrustedBoardUrlPermissionsStore.getState().isOriginTrusted('https://spamblocker.bitsocial.net')).toBe(true); + expect(useTrustedBoardUrlPermissionsStore.getState().getTrustedOrigins()).toEqual([ + expect.objectContaining({ + origin: 'https://spamblocker.bitsocial.net', + site: 'spamblocker.bitsocial.net', + }), + ]); + }); + + it('opens trusted iframe origins without asking again', async () => { + useTrustedBoardUrlPermissionsStore.getState().trustOrigin('https://spamblocker.bitsocial.net', 'spamblocker.bitsocial.net'); + testState.challenges = [ + createStoredChallenge({ + challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123', + type: 'url/iframe', + }), + ]; + + await renderModal(); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(container.textContent).not.toContain('wants to open'); + expect(container.querySelector('iframe')?.getAttribute('src')).toContain('https://spamblocker.bitsocial.net/api/v1/iframe/session-123?theme=dark'); + }); + it('allows localhost http iframe challenges for local spam blocker testing', async () => { const publication = createPublication(); testState.challenges = [ @@ -341,7 +378,12 @@ describe('ChallengeModal', () => { await renderModal(); - expect(confirmSpy).toHaveBeenCalledWith('mu wants to open localhost:3000.\n\nFor post: Subject'); + expect(confirmSpy).not.toHaveBeenCalled(); + expect(container.textContent).toContain('mu wants to open localhost:3000.'); + expect(container.textContent).not.toContain('For post:'); + expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}'); + + await clickButton('Open'); const iframe = container.querySelector('iframe'); expect(iframe).not.toBeNull(); @@ -374,6 +416,7 @@ describe('ChallengeModal', () => { ]; await renderModal(); + await clickButton('Open'); await act(async () => { window.dispatchEvent( @@ -405,6 +448,7 @@ describe('ChallengeModal', () => { ]; await renderModal(); + await clickButton('Open'); await act(async () => { window.dispatchEvent( @@ -442,11 +486,14 @@ describe('ChallengeModal', () => { await renderModal(); - expect(confirmSpy).toHaveBeenCalledWith(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000.\n\nFor post: Subject`); - expect(confirmSpy.mock.calls[0]?.[0]).not.toContain(longCommunityAddress); + expect(confirmSpy).not.toHaveBeenCalled(); + expect(container.textContent).toContain(`${getShortAddress(longCommunityAddress)} wants to open localhost:3000.`); + expect(container.textContent).not.toContain('For post:'); + expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}'); + expect(container.textContent).not.toContain(longCommunityAddress); }); - it('uses reply content in the iframe confirmation excerpt', async () => { + it('shows reply content in disabled fields instead of the iframe confirmation message', async () => { const publication = { ...createPublication(), content: '>>17\nA reply body with enough context', @@ -465,97 +512,15 @@ describe('ChallengeModal', () => { 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('uses inline iframe confirmation on Android instead of window.confirm', async () => { - const publication = createPublication(); - testState.capacitorPlatform = 'android'; - testState.publicationType = 'reply'; - testState.challenges = [ - createStoredChallenge( - { - challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123', - type: 'url/iframe', - }, - { - ...publication, - content: '>>17\nreply from android', - title: '', - }, - ), - ]; - - await renderModal(); - expect(confirmSpy).not.toHaveBeenCalled(); - expect(container.textContent).toContain('spamblocker.bitsocial.net'); - expect(container.textContent).toContain('mu wants to open spamblocker.bitsocial.net.\n\nFor reply: >>17 reply from android'); - expect(container.textContent).toContain('Open'); - expect(container.textContent).toContain('close'); - expect(container.textContent).not.toContain('Challenge for reply'); - expect(container.querySelector('iframe')).toBeNull(); - - await clickButton('Open'); - - const iframe = container.querySelector('iframe'); - expect(iframe).not.toBeNull(); - expect(iframe?.getAttribute('src')).toContain('https://spamblocker.bitsocial.net/api/v1/iframe/session-123?theme=dark'); - expect(testState.abandonCurrentChallengeMock).not.toHaveBeenCalled(); + expect(container.querySelector('#challenge-modal-title')?.textContent).toContain('Challenge for reply'); + expect(container.textContent).toContain('mu wants to open spamblocker.bitsocial.net.'); + expect(container.textContent).not.toContain('For reply:'); + expect(container.textContent).not.toContain('For {{publicationType}}: {{excerpt}}'); + expect(container.querySelector('textarea')?.value).toBe('>>17\nA reply body with enough context'); }); - it('uses inline iframe confirmation in Android WebView even when Capacitor reports web', async () => { - testState.capacitorPlatform = 'web'; - setNavigatorValue( - 'userAgent', - 'Mozilla/5.0 (Linux; Android 15; Pixel 9 Build/AP3A.241105.007) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/147.0.7727.137 Mobile Safari/537.36 wv', - ); - testState.publicationType = 'reply'; - testState.challenges = [ - createStoredChallenge( - { - challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123', - type: 'url/iframe', - }, - { - ...createPublication(), - content: '>>17\nreply from webview', - title: '', - }, - ), - ]; - - await renderModal(); - - expect(confirmSpy).not.toHaveBeenCalled(); - expect(container.textContent).toContain('mu wants to open spamblocker.bitsocial.net.\n\nFor reply: >>17 reply from webview'); - expect(container.querySelector('iframe')).toBeNull(); - - await clickButton('Open'); - - const iframe = container.querySelector('iframe'); - expect(iframe).not.toBeNull(); - expect(iframe?.getAttribute('src')).toContain('https://spamblocker.bitsocial.net/api/v1/iframe/session-123?theme=dark'); - }); - - it('abandons Android iframe challenges when inline confirmation is closed', async () => { - testState.capacitorPlatform = 'android'; + it('abandons iframe challenges when the custom confirmation is closed', async () => { testState.challenges = [ createStoredChallenge({ challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123', @@ -571,41 +536,8 @@ describe('ChallengeModal', () => { expect(container.querySelector('iframe')).toBeNull(); }); - it('uses inline iframe confirmation on Safari instead of window.confirm', async () => { - testState.publicationType = 'reply'; - setNavigatorValue('userAgent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15'); - setNavigatorValue('vendor', 'Apple Computer, Inc.'); - testState.challenges = [ - createStoredChallenge( - { - challenge: 'https://spamblocker.bitsocial.net/api/v1/iframe/session-123', - type: 'url/iframe', - }, - { - ...createPublication(), - content: '>>17\nreply from safari', - title: '', - }, - ), - ]; - - await renderModal(); - - expect(confirmSpy).not.toHaveBeenCalled(); - expect(container.textContent).toContain('mu wants to open spamblocker.bitsocial.net.\n\nFor reply: >>17 reply from safari'); - expect(container.querySelector('iframe')).toBeNull(); - - await clickButton('Open'); - - const iframe = container.querySelector('iframe'); - expect(iframe).not.toBeNull(); - expect(iframe?.getAttribute('src')).toContain('https://spamblocker.bitsocial.net/api/v1/iframe/session-123?theme=dark'); - }); - - it('shows iframe challenge errors inline on Safari instead of relying on alert', async () => { + it('shows iframe challenge errors inline instead of relying on alert', async () => { testState.account = { author: { address: '' } }; - setNavigatorValue('userAgent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15'); - setNavigatorValue('vendor', 'Apple Computer, Inc.'); testState.challenges = [ createStoredChallenge({ challenge: 'https://mintpass.org/auth?user={userAddress}', @@ -614,7 +546,6 @@ describe('ChallengeModal', () => { ]; await renderModal(); - await clickButton('Open'); expect(alertSpy).not.toHaveBeenCalled(); expect(container.textContent).toContain('Error: Unable to load challenge without your address. Please sign in and try again.'); @@ -625,24 +556,7 @@ describe('ChallengeModal', () => { expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce(); }); - it('alerts when iframe challenges need a signer address and the account is missing one', async () => { - testState.account = { author: { address: '' } }; - testState.challenges = [ - createStoredChallenge({ - challenge: 'https://mintpass.org/auth?user={userAddress}', - type: 'url/iframe', - }), - ]; - - await renderModal(); - - 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(); - }); - - it('abandons invalid iframe challenges and responds to Escape', async () => { + it('shows invalid iframe challenge errors inline and still responds to Escape', async () => { testState.challenges = [ createStoredChallenge({ challenge: 'http://example.com/unsafe', @@ -652,14 +566,15 @@ describe('ChallengeModal', () => { await renderModal(); - expect(alertSpy).toHaveBeenCalledWith('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported'); + expect(alertSpy).not.toHaveBeenCalled(); expect(confirmSpy).not.toHaveBeenCalled(); - expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce(); + expect(container.textContent).toContain('Error: Only HTTPS iframe challenges or localhost HTTP challenges are supported'); + expect(testState.abandonCurrentChallengeMock).not.toHaveBeenCalled(); await act(async () => { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); }); - expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledTimes(2); + expect(testState.abandonCurrentChallengeMock).toHaveBeenCalledOnce(); }); }); diff --git a/src/components/challenge-modal/challenge-modal.module.css b/src/components/challenge-modal/challenge-modal.module.css index 59582ee1..7fdd7834 100644 --- a/src/components/challenge-modal/challenge-modal.module.css +++ b/src/components/challenge-modal/challenge-modal.module.css @@ -113,6 +113,8 @@ } .iframeConsentWrapper { + flex-direction: column; + align-items: flex-start; justify-content: flex-start; margin: 10px 8px; } @@ -124,6 +126,20 @@ line-height: 1.35; } +.iframeTrustCheckbox { + display: flex; + align-items: flex-start; + gap: 6px; + margin-top: 10px; + line-height: 1.3; +} + +.iframeTrustCheckbox input[type="checkbox"] { + width: auto; + margin: 1px 0 0; + flex: 0 0 auto; +} + .iframe { width: 100%; height: 100%; @@ -138,6 +154,11 @@ margin-top: 10px; } +.iframeFooter span { + display: inline-flex; + gap: 5px; +} + .iframeInstruction { font-size: 11px; color: var(--text-secondary, #666); diff --git a/src/components/challenge-modal/challenge-modal.tsx b/src/components/challenge-modal/challenge-modal.tsx index 0e350d21..d85a060d 100644 --- a/src/components/challenge-modal/challenge-modal.tsx +++ b/src/components/challenge-modal/challenge-modal.tsx @@ -1,10 +1,10 @@ import { useRef, useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Challenge as ChallengeType, useAccount, useComment } from '@bitsocial/bitsocial-react-hooks'; -import { Capacitor } from '@capacitor/core'; -import { getPublicationPreview, getPublicationType, getVotePreview, type ChallengePublication } from '../../lib/utils/challenge-utils'; +import { getPublicationPreview, getPublicationType, getVotePreview } from '../../lib/utils/challenge-utils'; import useIsMobile from '../../hooks/use-is-mobile'; import useChallengesStore from '../../stores/use-challenges-store'; +import useTrustedBoardUrlPermissionsStore from '../../stores/use-trusted-board-url-permissions-store'; import useTheme from '../../hooks/use-theme'; import styles from './challenge-modal.module.css'; import capitalize from 'lodash/capitalize'; @@ -19,57 +19,14 @@ 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(); -const ANDROID_USER_AGENT_REGEX = /Android/; -const ANDROID_WEBVIEW_USER_AGENT_REGEX = /\bwv\b|Version\/\d+(?:\.\d+)?\s+Chrome\//; - -const isAndroidWebViewUserAgent = () => { - if (typeof navigator === 'undefined') return false; - return ANDROID_USER_AGENT_REGEX.test(navigator.userAgent) && ANDROID_WEBVIEW_USER_AGENT_REGEX.test(navigator.userAgent); -}; - -const isIosWebKitUserAgent = () => { - if (typeof navigator === 'undefined') return false; - return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); -}; - -const isDesktopSafariUserAgent = () => { - if (typeof navigator === 'undefined') return false; - const isAppleVendor = navigator.vendor === 'Apple Computer, Inc.'; - const isSafari = /Safari/.test(navigator.userAgent); - const isOtherBrowser = /Chrome|Chromium|CriOS|FxiOS|Edg|OPR|SamsungBrowser/.test(navigator.userAgent); - return isAppleVendor && isSafari && !isOtherBrowser; -}; - -const shouldUseInlineIframeConfirm = () => { - const platform = Capacitor.getPlatform(); - return platform === 'android' || platform === 'ios' || isAndroidWebViewUserAgent() || isIosWebKitUserAgent() || isDesktopSafariUserAgent(); -}; +const stripLegacyIframeConfirmDetails = (message: string) => message.split(/\r?\n\s*\r?\n/)[0] ?? message; const TextChallenge = ({ challenge }: { challenge: string }) =>
{challenge}
; @@ -136,36 +93,38 @@ const getIframeSessionId = (challengeUrl: string) => { interface IframeChallengeProps { challenge: string; - confirmKey: string; confirmMessage: string; - inlineConfirm: boolean; onCancel: () => void; onDone: () => void; onAutoComplete: (challengeAnswers: string[]) => void; onReady: () => void; openLabel: string; closeLabel: string; + rememberPermissionLabel: string; + publicationDetails: React.ReactNode; } -type IframeConfirmSource = 'native' | 'inline'; +type IframeOpenMode = 'trusted' | 'manual'; const IframeChallenge = ({ challenge, - confirmKey, confirmMessage, - inlineConfirm, onCancel, onDone, onAutoComplete, onReady, openLabel, closeLabel, + rememberPermissionLabel, + publicationDetails, }: IframeChallengeProps) => { const account = useAccount(); const [theme] = useTheme(); + const trustOrigin = useTrustedBoardUrlPermissionsStore((state) => state.trustOrigin); const [iframeUrlState, setIframeUrl] = useState(''); const [iframeOrigin, setIframeOrigin] = useState(''); const [inlineErrorMessage, setInlineErrorMessage] = useState(''); + const [rememberPermission, setRememberPermission] = useState(false); const iframeRef = useRef(null); const attemptedLoadRef = useRef(false); const mountedRef = useRef(false); @@ -192,18 +151,14 @@ const IframeChallenge = ({ ); const handleLoadIframe = useCallback( - (confirmSource: IframeConfirmSource) => { + (mode: IframeOpenMode) => { + if (attemptedLoadRef.current) return; const iframeUrl = challenge; if (!iframeUrl) return; const rejectChallenge = (message: string) => { - iframeChallengeConfirmDecisions.set(confirmKey, 'rejected'); - if (inlineConfirm) { - setInlineErrorMessage(message); - return; - } - alert(message); - onCancel(); + attemptedLoadRef.current = true; + setInlineErrorMessage(message); }; const rawUserAddress = account?.author?.address?.trim(); @@ -227,41 +182,31 @@ const IframeChallenge = ({ rejectChallenge('Error: Invalid URL for authentication challenge'); return; } - const decision = iframeChallengeConfirmDecisions.get(confirmKey); - if (decision === 'rejected') { - onCancel(); + + const isTrusted = useTrustedBoardUrlPermissionsStore.getState().isOriginTrusted(validatedUrl.origin); + if (!isTrusted && mode === 'trusted') { return; } - if (decision !== 'accepted') { - if (confirmSource === 'native' && !window.confirm(confirmMessage)) { - iframeChallengeConfirmDecisions.set(confirmKey, 'rejected'); - onCancel(); - return; - } - iframeChallengeConfirmDecisions.set(confirmKey, 'accepted'); + attemptedLoadRef.current = true; + if (!isTrusted && rememberPermission) { + trustOrigin(validatedUrl.origin, getReadableIframeUrl(replacedUrl), null); } openValidatedIframe(validatedUrl); }, - [account, challenge, confirmKey, confirmMessage, inlineConfirm, onCancel, openValidatedIframe, theme], + [account, challenge, openValidatedIframe, rememberPermission, theme, trustOrigin], ); useEffect(() => { - if (inlineConfirm) return; - if (attemptedLoadRef.current) return; - attemptedLoadRef.current = true; - handleLoadIframe('native'); - }, [handleLoadIframe, inlineConfirm]); + handleLoadIframe('trusted'); + }, [handleLoadIframe]); const handleInlineConfirm = useCallback(() => { - if (attemptedLoadRef.current) return; - attemptedLoadRef.current = true; - handleLoadIframe('inline'); + handleLoadIframe('manual'); }, [handleLoadIframe]); const handleInlineCancel = useCallback(() => { - iframeChallengeConfirmDecisions.set(confirmKey, 'rejected'); onCancel(); - }, [confirmKey, onCancel]); + }, [onCancel]); const sendThemeToIframe = useCallback(() => { postThemeToIframe(iframeRef.current, iframeOrigin, theme); @@ -302,28 +247,32 @@ const IframeChallenge = ({ }, [expectedSessionId, iframeOrigin, onAutoComplete]); if (!iframeUrlState) { - if (inlineConfirm) { - return ( - <> -
-
{inlineErrorMessage || confirmMessage}
-
-
- - {!inlineErrorMessage && ( - - )} - - -
- - ); - } - return null; + )} + + + + + ); } return ( @@ -347,7 +296,7 @@ const IframeChallenge = ({ ); }; -const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: ChallengeProps) => { +const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => { const { t } = useTranslation(); const challenges = challenge?.[0]?.challenges; @@ -377,12 +326,10 @@ const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: Challen })); const currentChallenge = challenges?.[currentChallengeIndex]; - const iframeChallengeKey = `${challengeId}:${currentChallengeIndex}:${currentChallenge?.challenge ?? ''}`; + const iframeChallengeKey = `${currentChallengeIndex}:${currentChallenge?.challenge ?? ''}`; const isTextChallenge = currentChallenge?.type === 'text/plain'; const isImageChallenge = currentChallenge?.type === 'image/png'; const isIframeChallenge = currentChallenge?.type === 'url/iframe'; - const inlineIframeConfirm = isIframeChallenge && shouldUseInlineIframeConfirm(); - const isIframePending = isIframeChallenge && !inlineIframeConfirm && readyIframeChallengeKey !== iframeChallengeKey; useEffect(() => { inputRef.current?.focus(); @@ -474,7 +421,7 @@ const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: Challen return null; } - const isIframeVisible = isIframeChallenge; + const isIframeVisible = isIframeChallenge && readyIframeChallengeKey === iframeChallengeKey; const containerClasses = [styles.container]; if (isIframeVisible) { @@ -490,14 +437,14 @@ const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: Challen 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 iframeConfirmMessage = stripLegacyIframeConfirmDetails( + t('iframe_challenge_confirm', { + board: displayCommunityAddress, + interpolation: { escapeValue: false }, + site: readableUrl || t('webpage'), + }), + ); + const challengeTitle = `Challenge for ${publicationType || t('post')}`; const publicationDetails = ( <> @@ -532,7 +479,6 @@ const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: Challen 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', }} > @@ -545,15 +491,15 @@ const Challenge = ({ challenge, challengeId, closeModal, abandonModal }: Challen ) : ( <> @@ -611,7 +557,7 @@ const ChallengeModal = () => { const challenge = current?.challenge; const challengeId = current?.id ?? 0; - return isOpen && challenge ? : null; + return isOpen && challenge ? : null; }; export default ChallengeModal; diff --git a/src/components/settings-modal/__tests__/settings-modal.test.tsx b/src/components/settings-modal/__tests__/settings-modal.test.tsx index 2d52f01e..f4122394 100644 --- a/src/components/settings-modal/__tests__/settings-modal.test.tsx +++ b/src/components/settings-modal/__tests__/settings-modal.test.tsx @@ -42,6 +42,10 @@ vi.mock('../subscriptions-setting', () => ({ default: () =>
subscriptions-settings
, })); +vi.mock('../trusted-board-links-setting', () => ({ + default: () =>
trusted-board-links-settings
, +})); + const LocationProbe = () => { const location = useLocation(); return
{location.pathname + location.hash}
; @@ -133,6 +137,7 @@ describe('SettingsModal', () => { expect(container.querySelector('[data-testid="media-hosting-settings-panel"]')).not.toBeNull(); expect(container.querySelector('[data-testid="account-settings"]')).not.toBeNull(); expect(container.querySelector('[data-testid="subscriptions-settings-panel"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="trusted-board-links-settings-panel"]')).not.toBeNull(); expect(container.querySelector('[data-testid="advanced-settings-panel"]')).not.toBeNull(); const collapseAllControl = Array.from(container.querySelectorAll('[role="button"]')).find((candidate) => @@ -150,6 +155,7 @@ describe('SettingsModal', () => { expect(container.querySelector('[data-testid="media-hosting-settings-panel"]')).toBeNull(); expect(container.querySelector('[data-testid="account-settings"]')).toBeNull(); expect(container.querySelector('[data-testid="subscriptions-settings-panel"]')).toBeNull(); + expect(container.querySelector('[data-testid="trusted-board-links-settings-panel"]')).toBeNull(); expect(container.querySelector('[data-testid="advanced-settings-panel"]')).toBeNull(); }); diff --git a/src/components/settings-modal/settings-modal.tsx b/src/components/settings-modal/settings-modal.tsx index 3f5b2ea5..8d4600c2 100644 --- a/src/components/settings-modal/settings-modal.tsx +++ b/src/components/settings-modal/settings-modal.tsx @@ -9,8 +9,16 @@ import InterfaceSettings from './interface-settings'; import MediaHostingSettings from './media-hosting-settings'; import AdvancedSettings from './advanced-settings'; import SubscriptionsSetting from './subscriptions-setting'; +import TrustedBoardLinksSetting from './trusted-board-links-setting'; -const allSectionIds = ['interface-settings', 'media-hosting-settings', 'account-settings', 'subscriptions-settings', 'advanced-settings']; +const allSectionIds = [ + 'interface-settings', + 'media-hosting-settings', + 'account-settings', + 'subscriptions-settings', + 'board-link-permissions-settings', + 'advanced-settings', +]; const hashToSection = (hash: string): string | null => { if (hash === 'crypto-address-settings' || hash === 'crypto-wallet-settings') return 'account-settings'; @@ -58,6 +66,7 @@ const SettingsModal = () => { const showMediaHostingSettings = visibleExpandedSections.has('media-hosting-settings'); const showAccountSettings = visibleExpandedSections.has('account-settings'); const showSubscriptionsSettings = visibleExpandedSections.has('subscriptions-settings'); + const showBoardLinkPermissionsSettings = visibleExpandedSections.has('board-link-permissions-settings'); const showAdvancedSettings = visibleExpandedSections.has('advanced-settings'); const allExpanded = useMemo(() => allSectionIds.every((id) => visibleExpandedSections.has(id)), [visibleExpandedSections]); @@ -162,6 +171,13 @@ const SettingsModal = () => { {showSubscriptionsSettings && } + + {showBoardLinkPermissionsSettings && }