mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(p2p settings): add browser p2p stats
This commit is contained in:
+2
-2
@@ -8,13 +8,13 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@bitsocial/bitsocial-react-hooks": "0.1.6",
|
||||
"@bitsocial/bitsocial-react-hooks": "0.1.8",
|
||||
"@capacitor/app": "7.0.1",
|
||||
"@capacitor/status-bar": "7.0.1",
|
||||
"@capawesome/capacitor-android-edge-to-edge-support": "7.2.2",
|
||||
"@chenglou/pretext": "0.0.5",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@pkcprotocol/pkc-js": "0.0.23",
|
||||
"@pkcprotocol/pkc-js": "0.0.29",
|
||||
"@react-spring/web": "10.0.3",
|
||||
"@types/node": "20.19.37",
|
||||
"@types/react": "18.2.25",
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من الشهر الماضي</1>]",
|
||||
"more_threads_last_year": "{{count}} مواضيع منذ {{currentTimeFilterName}} — [<1>عرض المزيد من المواضيع من السنة الماضية</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} يريد فتح {{site}}.\n\nبالنسبة إلى {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "غير متاح من هذه الشبكة. جرّب شبكة أخرى أو VPN."
|
||||
"media_hosting_provider_unavailable": "غير متاح من هذه الشبكة. جرّب شبكة أخرى أو VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত মাসের আরও থ্রেড দেখান</1>]",
|
||||
"more_threads_last_year": "{{count}} থ্রেড {{currentTimeFilterName}} থেকে — [<1>গত বছরের আরও থ্রেড দেখান</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} {{site}} খুলতে চায়।\n\n{{publicationType}}-এর জন্য: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "এই নেটওয়ার্ক থেকে উপলভ্য নয়। অন্য নেটওয়ার্ক বা একটি VPN চেষ্টা করুন।"
|
||||
"media_hosting_provider_unavailable": "এই নেটওয়ার্ক থেকে উপলভ্য নয়। অন্য নেটওয়ার্ক বা একটি VPN চেষ্টা করুন।",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Z této sítě není k dispozici. Zkuste jinou síť nebo VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Ikke tilgængelig fra dette netværk. Prøv et andet netværk eller en VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Von diesem Netzwerk aus nicht verfügbar. Versuche ein anderes Netzwerk oder ein VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από τον τελευταίο μήνα</1>]",
|
||||
"more_threads_last_year": "{{count}} νήματα από {{currentTimeFilterName}} — [<1>Προβολή περισσότερων νημάτων από το περασμένο έτος</1>]",
|
||||
"iframe_challenge_confirm": "Το {{board}} θέλει να ανοίξει το {{site}}.\n\nΓια {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "Δεν είναι διαθέσιμο από αυτό το δίκτυο. Δοκιμάστε άλλο δίκτυο ή VPN."
|
||||
"media_hosting_provider_unavailable": "Δεν είναι διαθέσιμο από αυτό το δίκτυο. Δοκιμάστε άλλο δίκτυο ή VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Unavailable from this network. Try another network or a VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "No disponible desde esta red. Prueba con otra red o una VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پستهای ماه گذشته</1>]",
|
||||
"more_threads_last_year": "{{count}} پست از {{currentTimeFilterName}} — [<1>نمایش بیشتر پستهای سال گذشته</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} میخواهد {{site}} را باز کند.\n\nبرای {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "از این شبکه در دسترس نیست. شبکه دیگری یا یک VPN را امتحان کنید."
|
||||
"media_hosting_provider_unavailable": "از این شبکه در دسترس نیست. شبکه دیگری یا یک VPN را امتحان کنید.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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:ää."
|
||||
"media_hosting_provider_unavailable": "Ei saatavilla tästä verkosta. Kokeile toista verkkoa tai VPN:ää.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Hindi available mula sa network na ito. Subukan ang ibang network o VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Indisponible depuis ce réseau. Essayez un autre réseau ou un VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהחודש שעבר</1>]",
|
||||
"more_threads_last_year": "{{count}} חוטים מאז {{currentTimeFilterName}} — [<1>הצג עוד חוטים מהשנה שעברה</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} רוצה לפתוח את {{site}}.\n\nעבור {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "לא זמין מהרשת הזו. נסו רשת אחרת או VPN."
|
||||
"media_hosting_provider_unavailable": "לא זמין מהרשת הזו. נסו רשת אחרת או VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>अगले महीने के अधिक थ्रेड्स दिखाएं</1>]",
|
||||
"more_threads_last_year": "{{count}} थ्रेड्स {{currentTimeFilterName}} से — [<1>पिछले साल के अधिक थ्रेड्स दिखाएं</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} {{site}} खोलना चाहता है।\n\n{{publicationType}} के लिए: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "इस नेटवर्क से उपलब्ध नहीं है। कोई दूसरा नेटवर्क या VPN आज़माएँ।"
|
||||
"media_hosting_provider_unavailable": "इस नेटवर्क से उपलब्ध नहीं है। कोई दूसरा नेटवर्क या VPN आज़माएँ।",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"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.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Tidak tersedia dari jaringan ini. Coba jaringan lain atau VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Non disponibile da questa rete. Prova un'altra rete o una VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>先月のスレッドをさらに表示</1>]",
|
||||
"more_threads_last_year": "{{count}} スレッド {{currentTimeFilterName}} から — [<1>去年のスレッドをさらに表示</1>]",
|
||||
"iframe_challenge_confirm": "{{board}}が{{site}}を開こうとしています。\n\n{{publicationType}}の場合: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "このネットワークからは利用できません。別のネットワークかVPNをお試しください。"
|
||||
"media_hosting_provider_unavailable": "このネットワークからは利用できません。別のネットワークかVPNをお試しください。",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>지난 달의 스레드 더 보기</1>]",
|
||||
"more_threads_last_year": "{{count}} 스레드 {{currentTimeFilterName}} 이후 — [<1>작년의 스레드 더 보기</1>]",
|
||||
"iframe_challenge_confirm": "{{board}}에서 {{site}} 열기를 요청했습니다.\n\n{{publicationType}}의 경우: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "이 네트워크에서는 사용할 수 없습니다. 다른 네트워크나 VPN을 시도해 보세요."
|
||||
"media_hosting_provider_unavailable": "이 네트워크에서는 사용할 수 없습니다. 다른 네트워크나 VPN을 시도해 보세요.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या महिन्यातील आणखी थ्रेड्स दाखवा</1>]",
|
||||
"more_threads_last_year": "{{count}} ध糀ं {{currentTimeFilterName}} पासून — [<1>गेल्या वर्षातील आणखी थ्रेड्स दाखवा</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} ला {{site}} उघडायचे आहे.\n\n{{publicationType}} साठी: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "या नेटवर्कवरून उपलब्ध नाही. दुसरे नेटवर्क किंवा VPN वापरून पाहा."
|
||||
"media_hosting_provider_unavailable": "या नेटवर्कवरून उपलब्ध नाही. दुसरे नेटवर्क किंवा VPN वापरून पाहा.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Niet beschikbaar via dit netwerk. Probeer een ander netwerk of een VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Ikke tilgjengelig fra dette nettverket. Prøv et annet nettverk eller en VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Niedostępne z tej sieci. Spróbuj użyć innej sieci lub VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Indisponível nesta rede. Tente outra rede ou uma VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Indisponibil din această rețea. Încearcă altă rețea sau un VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого месяца</1>]",
|
||||
"more_threads_last_year": "{{count}} темы с {{currentTimeFilterName}} — [<1>Показать больше тем с прошлого года</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} хочет открыть {{site}}.\n\nДля {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "Недоступно из этой сети. Попробуйте другую сеть или VPN."
|
||||
"media_hosting_provider_unavailable": "Недоступно из этой сети. Попробуйте другую сеть или VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Nuk disponohet nga ky rrjet. Provoni një rrjet tjetër ose një VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Inte tillgängligt från det här nätverket. Prova ett annat nätverk eller en VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} థ్రెడ్లు {{currentTimeFilterName}} నుండి — [<1>గత నెల నుండి మరిన్ని థ్రెడ్లను చూపించండి</1>]",
|
||||
"more_threads_last_year": "{{count}} థ్రెడ్లు {{currentTimeFilterName}} నుండి — [<1>గత సంవత్సరం నుండి మరిన్ని థ్రెడ్లను చూపించండి</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} {{site}} ను తెరవాలని కోరుతోంది.\n\n{{publicationType}} కోసం: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "ఈ నెట్వర్క్ నుండి అందుబాటులో లేదు. మరో నెట్వర్క్ లేదా VPN ప్రయత్నించండి."
|
||||
"media_hosting_provider_unavailable": "ఈ నెట్వర్క్ నుండి అందుబాటులో లేదు. మరో నెట్వర్క్ లేదా VPN ప్రయత్నించండి.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากเดือนที่แล้วเพิ่มเติม</1>]",
|
||||
"more_threads_last_year": "{{count}} กระทู้ตั้งแต่ {{currentTimeFilterName}} — [<1>แสดงกระทู้จากปีที่แล้วเพิ่มเติม</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} ต้องการเปิด {{site}}\n\nสำหรับ {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "ไม่สามารถใช้งานได้จากเครือข่ายนี้ ลองใช้เครือข่ายอื่นหรือ VPN"
|
||||
"media_hosting_provider_unavailable": "ไม่สามารถใช้งานได้จากเครือข่ายนี้ ลองใช้เครือข่ายอื่นหรือ VPN",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Bu ağdan kullanılamıyor. Başka bir ağ veya VPN deneyin.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого місяця</1>]",
|
||||
"more_threads_last_year": "{{count}} тем з {{currentTimeFilterName}} — [<1>Показати більше тем з минулого року</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} хоче відкрити {{site}}.\n\nДля {{publicationType}}: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "Недоступно з цієї мережі. Спробуйте іншу мережу або VPN."
|
||||
"media_hosting_provider_unavailable": "Недоступно з цієї мережі. Спробуйте іншу мережу або VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>پچھلے ماہ کے مزید تھریڈز دکھائیں</1>]",
|
||||
"more_threads_last_year": "{{count}} تھریڈز {{currentTimeFilterName}} سے — [<1>گزشتہ سال کے مزید تھریڈز دکھائیں</1>]",
|
||||
"iframe_challenge_confirm": "{{board}}، {{site}} کھولنا چاہتا ہے۔\n\n{{publicationType}} کے لیے: {{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "اس نیٹ ورک سے دستیاب نہیں۔ کوئی دوسرا نیٹ ورک یا VPN آزمائیں۔"
|
||||
"media_hosting_provider_unavailable": "اس نیٹ ورک سے دستیاب نہیں۔ کوئی دوسرا نیٹ ورک یا VPN آزمائیں۔",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"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>]",
|
||||
"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."
|
||||
"media_hosting_provider_unavailable": "Không khả dụng từ mạng này. Hãy thử mạng khác hoặc VPN.",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -365,5 +365,11 @@
|
||||
"more_threads_last_month": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多上个月的主题</1>]",
|
||||
"more_threads_last_year": "{{count}} 主题自 {{currentTimeFilterName}} — [<1>显示更多去年的主题</1>]",
|
||||
"iframe_challenge_confirm": "{{board}} 想要打开 {{site}}。\n\n对于 {{publicationType}}:{{excerpt}}",
|
||||
"media_hosting_provider_unavailable": "此网络无法使用。请尝试其他网络或 VPN。"
|
||||
"media_hosting_provider_unavailable": "此网络无法使用。请尝试其他网络或 VPN。",
|
||||
"p2p_stats": "P2P stats",
|
||||
"enable_pure_p2p": "Enable pure P2P",
|
||||
"enable_pure_p2p_tip": "Use a browser P2P node instead of gateway compatibility mode.",
|
||||
"p2p_stats_loading": "Loading P2P stats...",
|
||||
"p2p_stats_updated": "Updated",
|
||||
"p2p_stats_starting": "P2P stats appear after the P2P client starts."
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@ import SettingsModal from '../settings-modal';
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
account: {
|
||||
pkcOptions: {
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
},
|
||||
} as Record<string, any>,
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||
useAccount: () => testState.account,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
@@ -42,6 +54,10 @@ vi.mock('../subscriptions-setting', () => ({
|
||||
default: () => <div data-testid='subscriptions-settings-panel'>subscriptions-settings</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../p2p-stats-settings', () => ({
|
||||
default: () => <div data-testid='p2p-stats-settings-panel'>p2p-stats-settings</div>,
|
||||
}));
|
||||
|
||||
const LocationProbe = () => {
|
||||
const location = useLocation();
|
||||
return <div data-testid='location'>{location.pathname + location.hash}</div>;
|
||||
@@ -134,6 +150,7 @@ describe('SettingsModal', () => {
|
||||
expect(container.querySelector('[data-testid="account-settings"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="subscriptions-settings-panel"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="advanced-settings-panel"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="p2p-stats-settings-panel"]')).not.toBeNull();
|
||||
|
||||
const collapseAllControl = Array.from(container.querySelectorAll('[role="button"]')).find((candidate) =>
|
||||
(candidate.textContent ?? '').includes('collapse_all_settings'),
|
||||
@@ -151,6 +168,13 @@ describe('SettingsModal', () => {
|
||||
expect(container.querySelector('[data-testid="account-settings"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="subscriptions-settings-panel"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="advanced-settings-panel"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="p2p-stats-settings-panel"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('opens the p2p stats section from its hash', () => {
|
||||
render('/all/settings#p2p-stats-settings');
|
||||
|
||||
expect(container.querySelector('[data-testid="p2p-stats-settings-panel"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('closes the modal when the overlay is clicked', async () => {
|
||||
|
||||
@@ -84,6 +84,7 @@ const clickButton = async (text: string) => {
|
||||
describe('AdvancedSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
testState.account = {
|
||||
mediaIpfsGatewayUrl: 'https://media.old.example',
|
||||
chainProviders: {
|
||||
@@ -192,6 +193,66 @@ describe('AdvancedSettings', () => {
|
||||
expect(textInputs[2]?.value).toBe('/tmp/connected-node');
|
||||
});
|
||||
|
||||
it('saves the browser pure p2p toggle through advanced settings', async () => {
|
||||
await renderSettings(false);
|
||||
|
||||
const checkbox = container.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
expect(checkbox?.checked).toBe(false);
|
||||
expect(container.textContent).not.toContain('pure P2P:');
|
||||
expect(checkbox?.closest('label')?.nextElementSibling?.textContent).toBe('enable_pure_p2p_tip');
|
||||
|
||||
await act(async () => {
|
||||
checkbox?.click();
|
||||
});
|
||||
await clickButton('save_advanced_settings');
|
||||
|
||||
expect(testState.setAccountMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pkcOptions: expect.objectContaining({
|
||||
httpRoutersOptions: ['https://router.old.example'],
|
||||
ipfsGatewayUrls: undefined,
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
pkcRpcClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(localStorage.getItem('5chan:pure-p2p-browser-enabled')).toBe('true');
|
||||
expect(reloadMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('saves gateway mode defaults when browser pure p2p is disabled', async () => {
|
||||
testState.account = {
|
||||
mediaIpfsGatewayUrl: 'https://media.old.example',
|
||||
pkcOptions: {
|
||||
httpRoutersOptions: ['https://peers.pleb.bot'],
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
},
|
||||
};
|
||||
|
||||
await renderSettings(false);
|
||||
|
||||
const checkbox = container.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
expect(checkbox?.checked).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
checkbox?.click();
|
||||
});
|
||||
await clickButton('save_advanced_settings');
|
||||
|
||||
expect(testState.setAccountMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pkcOptions: expect.objectContaining({
|
||||
httpRoutersOptions: ['https://peers.pleb.bot'],
|
||||
ipfsGatewayUrls: ['https://ipfsgateway.xyz', 'https://gateway.plebpubsub.xyz', 'https://gateway.forumindex.com'],
|
||||
libp2pJsClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(localStorage.getItem('5chan:pure-p2p-browser-enabled')).toBe('false');
|
||||
});
|
||||
|
||||
it('toggles the node rpc instructions panel', async () => {
|
||||
await renderSettings(false);
|
||||
|
||||
|
||||
@@ -78,6 +78,19 @@
|
||||
padding-left: 1px;
|
||||
}
|
||||
|
||||
.pureP2PSettings {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pureP2PSettings .settingTip {
|
||||
margin: 2px 0 5px 0;
|
||||
padding-left: 19px;
|
||||
}
|
||||
|
||||
.pureP2PCheckbox {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.p2pRPCSettings button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { memo, RefObject, useRef, useState } from 'react';
|
||||
import { setAccount, useAccount, usePkcRpcSettings } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getBrowserGatewayPkcOptions, getBrowserPureP2PPkcOptions, setPureP2PBrowserPreference } from '../../../lib/p2p-browser-config';
|
||||
import { canConfigureBrowserPureP2P, isBrowserPureP2PEnabled } from '../../../lib/p2p-runtime';
|
||||
import styles from './advanced-settings.module.css';
|
||||
|
||||
interface SettingsProps {
|
||||
@@ -12,6 +14,7 @@ interface SettingsProps {
|
||||
solRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
p2pRpcRef?: RefObject<HTMLInputElement>;
|
||||
p2pDataPathRef?: RefObject<HTMLInputElement>;
|
||||
pureP2PBrowserRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
type AccountProtocolOptions = {
|
||||
@@ -19,6 +22,8 @@ type AccountProtocolOptions = {
|
||||
dataPath?: string;
|
||||
httpRoutersOptions?: string[];
|
||||
ipfsGatewayUrls?: string[];
|
||||
kuboRpcClientsOptions?: unknown[];
|
||||
libp2pJsClientsOptions?: unknown[];
|
||||
pkcRpcClientsOptions?: string[];
|
||||
pubsubHttpClientsOptions?: string[];
|
||||
pubsubKuboRpcClientsOptions?: string[];
|
||||
@@ -210,6 +215,21 @@ const P2pDataPathSettings = ({ p2pDataPathRef }: SettingsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PureP2PBrowserSettings = ({ pureP2PBrowserRef }: SettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount() as AccountShape | undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.pureP2PSettings}>
|
||||
<label>
|
||||
<input className={styles.pureP2PCheckbox} type='checkbox' defaultChecked={isBrowserPureP2PEnabled(account)} ref={pureP2PBrowserRef} />
|
||||
{t('enable_pure_p2p')}
|
||||
</label>
|
||||
<div className={styles.settingTip}>{t('enable_pure_p2p_tip')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isElectron = window.electronApi?.isElectron === true;
|
||||
|
||||
const AdvancedSettings = () => {
|
||||
@@ -225,6 +245,7 @@ const AdvancedSettings = () => {
|
||||
const httpRoutersRef = useRef<HTMLTextAreaElement>(null);
|
||||
const p2pRpcRef = useRef<HTMLInputElement>(null);
|
||||
const p2pDataPathRef = useRef<HTMLInputElement>(null);
|
||||
const pureP2PBrowserRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSave = async () => {
|
||||
const ipfsGatewayUrls = ipfsGatewayUrlsRef.current?.value
|
||||
@@ -256,6 +277,7 @@ const AdvancedSettings = () => {
|
||||
|
||||
const pkcRpcClientsOptions = p2pRpcRef.current?.value.trim() ? [p2pRpcRef.current.value.trim()] : undefined;
|
||||
const dataPath = p2pDataPathRef.current?.value.trim() || undefined;
|
||||
const pureP2PBrowserPreference = canConfigureBrowserPureP2P() ? pureP2PBrowserRef.current?.checked : undefined;
|
||||
|
||||
const chainProviders: Record<string, { urls: string[] | undefined; chainId: number }> = {};
|
||||
if (ethRpcUrls && ethRpcUrls.length > 0) {
|
||||
@@ -265,20 +287,44 @@ const AdvancedSettings = () => {
|
||||
chainProviders.sol = { urls: solRpcUrls, chainId: 101 };
|
||||
}
|
||||
|
||||
let pkcOptions: AccountProtocolOptions = {
|
||||
...protocolOptions,
|
||||
ipfsGatewayUrls,
|
||||
pubsubKuboRpcClientsOptions,
|
||||
httpRoutersOptions,
|
||||
pkcRpcClientsOptions,
|
||||
dataPath,
|
||||
};
|
||||
|
||||
if (pureP2PBrowserPreference !== undefined) {
|
||||
if (pureP2PBrowserPreference) {
|
||||
const pureP2POptions = getBrowserPureP2PPkcOptions();
|
||||
pkcOptions = {
|
||||
...pkcOptions,
|
||||
...pureP2POptions,
|
||||
httpRoutersOptions: httpRoutersOptions?.length ? httpRoutersOptions : pureP2POptions.httpRoutersOptions,
|
||||
pkcRpcClientsOptions: undefined,
|
||||
};
|
||||
} else {
|
||||
const gatewayOptions = getBrowserGatewayPkcOptions();
|
||||
pkcOptions = {
|
||||
...pkcOptions,
|
||||
...gatewayOptions,
|
||||
ipfsGatewayUrls: ipfsGatewayUrls?.length ? ipfsGatewayUrls : gatewayOptions.ipfsGatewayUrls,
|
||||
pubsubKuboRpcClientsOptions: pubsubKuboRpcClientsOptions?.length ? pubsubKuboRpcClientsOptions : gatewayOptions.pubsubKuboRpcClientsOptions,
|
||||
httpRoutersOptions: httpRoutersOptions?.length ? httpRoutersOptions : gatewayOptions.httpRoutersOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await setAccount({
|
||||
...account,
|
||||
mediaIpfsGatewayUrl,
|
||||
chainProviders,
|
||||
pkcOptions: {
|
||||
...protocolOptions,
|
||||
ipfsGatewayUrls,
|
||||
pubsubKuboRpcClientsOptions,
|
||||
httpRoutersOptions,
|
||||
pkcRpcClientsOptions,
|
||||
dataPath,
|
||||
},
|
||||
pkcOptions,
|
||||
});
|
||||
if (pureP2PBrowserPreference !== undefined) setPureP2PBrowserPreference(pureP2PBrowserPreference);
|
||||
alert('Options saved, reloading...');
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
@@ -293,6 +339,7 @@ const AdvancedSettings = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{canConfigureBrowserPureP2P() && <PureP2PBrowserSettings pureP2PBrowserRef={pureP2PBrowserRef} />}
|
||||
<div className={styles.category}>
|
||||
<span className={styles.categoryTitle}>IPFS gateways:</span>
|
||||
<span className={styles.categorySettings}>
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import * as React from 'react';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
account: {} as Record<string, any>,
|
||||
rpcSettings: { state: 'disconnected' } as Record<string, any>,
|
||||
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||
setAccount: (account: unknown) => testState.setAccountMock(account),
|
||||
useAccount: () => testState.account,
|
||||
usePkcRpcSettings: () => testState.rpcSettings,
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const loadComponent = async (isElectron = false) => {
|
||||
vi.resetModules();
|
||||
window.electronApi = isElectron ? ({ isElectron: true } as Window['electronApi']) : undefined;
|
||||
window.isElectron = isElectron;
|
||||
return (await import('../p2p-stats-settings')).default;
|
||||
};
|
||||
|
||||
const renderSettings = async (isElectron = false) => {
|
||||
const P2PStatsSettings = await loadComponent(isElectron);
|
||||
await act(async () => {
|
||||
root.render(createElement(P2PStatsSettings));
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
describe('P2PStatsSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
window.electronApi = undefined;
|
||||
window.isElectron = false;
|
||||
testState.account = {
|
||||
id: 'account-1',
|
||||
author: { address: 'author', wallets: {} },
|
||||
pkcOptions: {
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
},
|
||||
};
|
||||
testState.rpcSettings = { state: 'disconnected' };
|
||||
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('renders browser libp2p stats from the active PKC client', async () => {
|
||||
testState.account = {
|
||||
...testState.account,
|
||||
pkcOptions: {
|
||||
httpRoutersOptions: ['https://router.example'],
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
},
|
||||
pkc: {
|
||||
clients: {
|
||||
libp2pJsClients: {
|
||||
libp2pjs: {
|
||||
key: 'libp2pjs',
|
||||
heliaWithKuboRpcClientFunctions: {
|
||||
add: async () => {
|
||||
throw new Error("Helia 'add' is not supported at the moment in pkc-js API");
|
||||
},
|
||||
},
|
||||
_helia: {
|
||||
libp2p: {
|
||||
getConnections: () => ['connection-1'],
|
||||
getMultiaddrs: () => ['/ip4/127.0.0.1/tcp/4001'],
|
||||
getPeers: () => ['peer-1', 'peer-2'],
|
||||
metrics: {
|
||||
toJSON: () => ({
|
||||
helia_bitswap_data_received_bytes: { global: 2048, peer1: 2048 },
|
||||
helia_bitswap_sent_data_bytes_total: 1024,
|
||||
}),
|
||||
},
|
||||
peerId: { toString: () => 'self-peer' },
|
||||
services: {
|
||||
pubsub: {
|
||||
getPeers: () => ['peer-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
routers: [
|
||||
{
|
||||
async provide() {
|
||||
// noop
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await renderSettings(false);
|
||||
await act(async () => Promise.resolve());
|
||||
|
||||
expect(container.textContent).toContain('leeching');
|
||||
expect(container.textContent).not.toContain('browser Helia');
|
||||
expect(container.textContent).not.toContain('seed mode');
|
||||
expect(container.textContent).not.toContain('status');
|
||||
expect(container.textContent).toContain('self-peer');
|
||||
expect(container.textContent).toContain('2 peers');
|
||||
expect(container.textContent).toContain('downloaded data');
|
||||
expect(container.textContent).toContain('2.00 KB');
|
||||
expect(container.textContent).toContain('uploaded data');
|
||||
expect(container.textContent).toContain('1.00 KB');
|
||||
expect(container.textContent).not.toContain('pubsub topics');
|
||||
expect(container.textContent).not.toContain('topic subscribers');
|
||||
});
|
||||
|
||||
it('reads browser transfer counters from Helia bitswap ledgers', async () => {
|
||||
testState.account = {
|
||||
...testState.account,
|
||||
pkcOptions: {
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
},
|
||||
pkc: {
|
||||
clients: {
|
||||
libp2pJsClients: {
|
||||
libp2pjs: {
|
||||
key: 'libp2pjs',
|
||||
_helia: {
|
||||
blockstore: {
|
||||
child: {
|
||||
blockBrokers: [
|
||||
{
|
||||
bitswap: {
|
||||
peerWantLists: {
|
||||
ledgerMap: new Map([['peer-1', { bytesReceived: 4096, bytesSent: 2048 }]]),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
libp2p: {
|
||||
getConnections: () => [],
|
||||
getMultiaddrs: () => [],
|
||||
getPeers: () => [],
|
||||
peerId: { toString: () => 'self-peer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await renderSettings(false);
|
||||
await act(async () => Promise.resolve());
|
||||
|
||||
expect(container.textContent).toContain('downloaded data');
|
||||
expect(container.textContent).toContain('4.00 KB');
|
||||
expect(container.textContent).toContain('uploaded data');
|
||||
expect(container.textContent).toContain('2.00 KB');
|
||||
});
|
||||
|
||||
it('reports seeding only when browser Helia can add and publish provider records', async () => {
|
||||
testState.account = {
|
||||
...testState.account,
|
||||
pkcOptions: {
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
},
|
||||
pkc: {
|
||||
clients: {
|
||||
libp2pJsClients: {
|
||||
libp2pjs: {
|
||||
key: 'libp2pjs',
|
||||
heliaWithKuboRpcClientFunctions: {
|
||||
add: async () => ({ cid: 'cid' }),
|
||||
},
|
||||
_helia: {
|
||||
libp2p: {
|
||||
getConnections: () => [],
|
||||
getMultiaddrs: () => [],
|
||||
getPeers: () => [],
|
||||
peerId: { toString: () => 'self-peer' },
|
||||
},
|
||||
routing: {
|
||||
routers: [
|
||||
{
|
||||
provide: async (cid: unknown) => cid,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await renderSettings(false);
|
||||
await act(async () => Promise.resolve());
|
||||
|
||||
expect(container.textContent).toContain('seeding');
|
||||
expect(container.textContent).not.toContain('seed mode');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './p2p-stats-settings';
|
||||
@@ -0,0 +1,54 @@
|
||||
.content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.setting {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.settingTip {
|
||||
display: block;
|
||||
font-size: 0.85em;
|
||||
margin: 4px 0 0 20px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin: 6px 0 0;
|
||||
width: calc(100% - 8px);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.stats th {
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stats th,
|
||||
.stats td {
|
||||
padding: 2px 4px 3px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.statName {
|
||||
width: 34%;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.statsMeta {
|
||||
margin-top: 5px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.statsMeta button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-text-color, #b00020);
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
import { memo, useEffect, useReducer } from 'react';
|
||||
import { useAccount, usePkcRpcSettings } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getP2PRuntimeMode, type P2PRuntimeMode } from '../../../lib/p2p-runtime';
|
||||
import styles from './p2p-stats-settings.module.css';
|
||||
|
||||
type AccountShape = Record<string, any>;
|
||||
|
||||
type StatRow = {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type StatsState = {
|
||||
error?: string;
|
||||
loading: boolean;
|
||||
rows: StatRow[];
|
||||
updatedAt?: number;
|
||||
};
|
||||
|
||||
type StatsAction =
|
||||
| {
|
||||
type: 'loading';
|
||||
}
|
||||
| {
|
||||
rows: StatRow[];
|
||||
timestamp: number;
|
||||
type: 'loaded';
|
||||
}
|
||||
| {
|
||||
error: string;
|
||||
timestamp: number;
|
||||
type: 'failed';
|
||||
};
|
||||
|
||||
type Libp2pClientShape = {
|
||||
_helia?: {
|
||||
libp2p?: {
|
||||
getConnections?: () => unknown[] | Promise<unknown[]>;
|
||||
getMultiaddrs?: () => unknown[] | Promise<unknown[]>;
|
||||
getPeers?: () => unknown[] | Promise<unknown[]>;
|
||||
peerId?: { toString: () => string };
|
||||
services?: {
|
||||
pubsub?: {
|
||||
getPeers?: () => unknown[] | Promise<unknown[]>;
|
||||
};
|
||||
};
|
||||
metrics?: unknown;
|
||||
};
|
||||
metrics?: unknown;
|
||||
routing?: {
|
||||
routers?: unknown[];
|
||||
};
|
||||
};
|
||||
heliaWithKuboRpcClientFunctions?: {
|
||||
add?: unknown;
|
||||
};
|
||||
key?: string;
|
||||
};
|
||||
|
||||
type TransferStats = {
|
||||
downloadedBytes?: number;
|
||||
uploadedBytes?: number;
|
||||
};
|
||||
|
||||
const KUBO_API_URL = 'http://localhost:50019/api/v0';
|
||||
const STATS_REFRESH_MS = 5000;
|
||||
const MAX_TRANSFER_COUNTER_DEPTH = 10;
|
||||
const MAX_TRANSFER_COUNTER_OBJECTS = 400;
|
||||
|
||||
const statsReducer = (state: StatsState, action: StatsAction): StatsState => {
|
||||
if (action.type === 'loading') return { ...state, error: undefined, loading: true };
|
||||
if (action.type === 'loaded') return { loading: false, rows: action.rows, updatedAt: action.timestamp };
|
||||
return { error: action.error, loading: false, rows: [], updatedAt: action.timestamp };
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown, fallback = 'Error') => (error instanceof Error ? error.message : String(error || fallback));
|
||||
|
||||
const formatCount = (count: number, singular: string, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
|
||||
|
||||
const formatBytes = (value: unknown) => {
|
||||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue)) return String(value ?? 'unknown');
|
||||
if (numericValue < 1024) return `${numericValue} B`;
|
||||
const units = ['KB', 'MB', 'GB', 'TB'];
|
||||
let size = numericValue / 1024;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const formatRate = (value: unknown) => `${formatBytes(value)}/s`;
|
||||
|
||||
const stringifyList = (items?: unknown[], maxItems = 5) => {
|
||||
if (!items?.length) return 'none';
|
||||
const values = items.map((item) => String(item));
|
||||
const visibleValues = values.slice(0, maxItems);
|
||||
return values.length > maxItems ? `${visibleValues.join(', ')} +${values.length - maxItems} more` : visibleValues.join(', ');
|
||||
};
|
||||
|
||||
const getFirstObjectValue = <T,>(value?: Record<string, T>) => (value ? Object.values(value)[0] : undefined);
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object';
|
||||
|
||||
const getFiniteNumber = (value: unknown) => {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
const numericValue = typeof value === 'bigint' ? Number(value) : Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : undefined;
|
||||
};
|
||||
|
||||
const addTransferStats = (stats: TransferStats, direction: keyof TransferStats, value: unknown) => {
|
||||
const numericValue = getFiniteNumber(value);
|
||||
if (numericValue === undefined) return;
|
||||
stats[direction] = (stats[direction] ?? 0) + numericValue;
|
||||
};
|
||||
|
||||
const getEntries = (value: unknown): [string, unknown][] => {
|
||||
try {
|
||||
if (value instanceof Map) return Array.from(value.entries()).map(([key, entry]) => [String(key), entry]);
|
||||
if (Array.isArray(value)) return value.map((entry, index) => [String(index), entry]);
|
||||
if (isRecord(value)) return Object.entries(value);
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const toArray = (value: unknown): unknown[] => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value && typeof value === 'object' && Symbol.iterator in value) return Array.from(value as Iterable<unknown>);
|
||||
return [];
|
||||
};
|
||||
|
||||
const getSafeArray = async (getValue?: () => unknown[] | Promise<unknown[]> | undefined): Promise<unknown[]> => {
|
||||
try {
|
||||
return toArray(getValue ? await getValue() : undefined);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getTransferStatsFromHeliaCounters = (helia: unknown): TransferStats => {
|
||||
const stats: TransferStats = {};
|
||||
const visited = new WeakSet<object>();
|
||||
let objectsVisited = 0;
|
||||
|
||||
const visit = (value: unknown, depth: number) => {
|
||||
try {
|
||||
if (!isRecord(value) || visited.has(value) || depth > MAX_TRANSFER_COUNTER_DEPTH || objectsVisited > MAX_TRANSFER_COUNTER_OBJECTS) return;
|
||||
visited.add(value);
|
||||
objectsVisited++;
|
||||
|
||||
if ('bytesReceived' in value || 'bytesSent' in value) {
|
||||
addTransferStats(stats, 'downloadedBytes', value.bytesReceived);
|
||||
addTransferStats(stats, 'uploadedBytes', value.bytesSent);
|
||||
}
|
||||
|
||||
for (const [key, entry] of getEntries(value)) {
|
||||
if (typeof entry === 'function' || key === 'logger' || key === 'log' || key === 'events' || key === 'datastore' || key === 'routing') continue;
|
||||
visit(entry, depth + 1);
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
visit(helia, 0);
|
||||
return stats;
|
||||
};
|
||||
|
||||
const classifyTransferMetricPath = (path: string[]) => {
|
||||
const normalizedPath = path
|
||||
.join('_')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '');
|
||||
if (normalizedPath.includes('rate')) return undefined;
|
||||
if (
|
||||
normalizedPath.includes('totalin') ||
|
||||
normalizedPath.includes('bytesreceived') ||
|
||||
normalizedPath.includes('receivedbytes') ||
|
||||
normalizedPath.includes('datareceivedbytes')
|
||||
) {
|
||||
return 'downloadedBytes' as const;
|
||||
}
|
||||
if (
|
||||
normalizedPath.includes('totalout') ||
|
||||
normalizedPath.includes('bytessent') ||
|
||||
normalizedPath.includes('sentbytes') ||
|
||||
normalizedPath.includes('datasentbytes') ||
|
||||
normalizedPath.includes('sentdatabytes')
|
||||
) {
|
||||
return 'uploadedBytes' as const;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getMetricSnapshot = async (source: unknown) => {
|
||||
if (!isRecord(source)) return source;
|
||||
for (const method of ['getMetrics', 'getMetricValues', 'toJSON']) {
|
||||
const candidate = source[method];
|
||||
if (typeof candidate !== 'function') continue;
|
||||
try {
|
||||
const snapshot = await candidate.call(source);
|
||||
if (snapshot !== undefined) return snapshot;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
const getTransferStatsFromMetricSnapshot = (snapshot: unknown): TransferStats => {
|
||||
const stats: TransferStats = {};
|
||||
const visited = new WeakSet<object>();
|
||||
|
||||
const visit = (value: unknown, path: string[], depth: number) => {
|
||||
const direction = classifyTransferMetricPath(path);
|
||||
const numericValue = getFiniteNumber(value);
|
||||
if (direction && numericValue !== undefined) {
|
||||
addTransferStats(stats, direction, numericValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(value) || visited.has(value) || depth > MAX_TRANSFER_COUNTER_DEPTH) return;
|
||||
visited.add(value);
|
||||
|
||||
if (direction && 'global' in value) {
|
||||
addTransferStats(stats, direction, value.global);
|
||||
return;
|
||||
}
|
||||
|
||||
if (direction && 'value' in value) {
|
||||
addTransferStats(stats, direction, value.value);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, entry] of getEntries(value)) visit(entry, [...path, key], depth + 1);
|
||||
};
|
||||
|
||||
visit(snapshot, [], 0);
|
||||
return stats;
|
||||
};
|
||||
|
||||
const mergeTransferStats = (primary: TransferStats, fallback: TransferStats): TransferStats => ({
|
||||
downloadedBytes: primary.downloadedBytes ?? fallback.downloadedBytes,
|
||||
uploadedBytes: primary.uploadedBytes ?? fallback.uploadedBytes,
|
||||
});
|
||||
|
||||
const getBrowserTransferStats = async (client?: Libp2pClientShape): Promise<TransferStats> => {
|
||||
try {
|
||||
const helia = client?._helia;
|
||||
const counterStats = getTransferStatsFromHeliaCounters(helia);
|
||||
const metricSources = [helia?.metrics, helia?.libp2p?.metrics].filter(Boolean);
|
||||
let metricStats: TransferStats = {};
|
||||
|
||||
for (const source of metricSources) {
|
||||
metricStats = mergeTransferStats(metricStats, getTransferStatsFromMetricSnapshot(await getMetricSnapshot(source)));
|
||||
}
|
||||
|
||||
return mergeTransferStats(counterStats, metricStats);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const getFunctionSource = (value: unknown) => {
|
||||
if (typeof value !== 'function') return undefined;
|
||||
try {
|
||||
return Function.prototype.toString.call(value).toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const hasSupportedAdd = (client?: Libp2pClientShape) => {
|
||||
const add = client?.heliaWithKuboRpcClientFunctions?.add;
|
||||
const source = getFunctionSource(add);
|
||||
return typeof add === 'function' && !source?.includes('not supported') && !source?.includes('unsupported');
|
||||
};
|
||||
|
||||
const isKnownNoopProvide = (provide: unknown) => {
|
||||
const source = getFunctionSource(provide);
|
||||
if (typeof provide !== 'function') return true;
|
||||
return Boolean(source?.includes('noop') || source?.replace(/\s/g, '') === 'asyncprovide(){}');
|
||||
};
|
||||
|
||||
const hasProviderPublishingRouter = (client?: Libp2pClientShape) =>
|
||||
(client?._helia?.routing?.routers ?? []).some((router) => isRecord(router) && !isKnownNoopProvide(router.provide));
|
||||
|
||||
const getBrowserMode = (client?: Libp2pClientShape) => {
|
||||
if (!client) return 'unknown';
|
||||
return hasSupportedAdd(client) && hasProviderPublishingRouter(client) ? 'seeding' : 'leeching';
|
||||
};
|
||||
|
||||
const getBrowserLibp2pStats = async (account?: AccountShape): Promise<StatRow[]> => {
|
||||
const client = getFirstObjectValue(account?.pkc?.clients?.libp2pJsClients) as Libp2pClientShape | undefined;
|
||||
const libp2p = client?._helia?.libp2p;
|
||||
const pubsub = libp2p?.services?.pubsub;
|
||||
const [peers, connections, multiaddrs, pubsubPeers, transferStats] = await Promise.all([
|
||||
getSafeArray(() => libp2p?.getPeers?.()),
|
||||
getSafeArray(() => libp2p?.getConnections?.()),
|
||||
getSafeArray(() => libp2p?.getMultiaddrs?.()),
|
||||
getSafeArray(() => pubsub?.getPeers?.()),
|
||||
getBrowserTransferStats(client),
|
||||
]);
|
||||
|
||||
return [
|
||||
{ name: 'mode', value: getBrowserMode(client) },
|
||||
{ name: 'peer id', value: libp2p?.peerId?.toString() ?? 'unknown' },
|
||||
{ name: 'client key', value: client?.key ?? 'unknown' },
|
||||
{ name: 'connected peers', value: formatCount(peers.length, 'peer') },
|
||||
{ name: 'connections', value: formatCount(connections.length, 'connection') },
|
||||
{ name: 'downloaded data', value: transferStats.downloadedBytes === undefined ? 'unknown' : formatBytes(transferStats.downloadedBytes) },
|
||||
{ name: 'uploaded data', value: transferStats.uploadedBytes === undefined ? 'unknown' : formatBytes(transferStats.uploadedBytes) },
|
||||
{ name: 'listen addresses', value: stringifyList(multiaddrs) },
|
||||
{ name: 'pubsub peers', value: formatCount(pubsubPeers.length, 'peer') },
|
||||
{ name: 'routers', value: stringifyList(account?.pkcOptions?.httpRoutersOptions) },
|
||||
];
|
||||
};
|
||||
|
||||
const kuboPostJson = async (path: string, params?: Record<string, string | boolean>, signal?: AbortSignal) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params ?? {})) searchParams.set(key, String(value));
|
||||
const query = searchParams.toString();
|
||||
const response = await fetch(`${KUBO_API_URL}/${path}${query ? `?${query}` : ''}`, { method: 'POST', signal });
|
||||
if (!response.ok) throw new Error(`Kubo ${path} returned ${response.status}`);
|
||||
const text = await response.text();
|
||||
const firstJsonLine = text
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean);
|
||||
return firstJsonLine ? JSON.parse(firstJsonLine) : {};
|
||||
};
|
||||
|
||||
const getElectronKuboStats = async (rpcState?: string, signal?: AbortSignal): Promise<StatRow[]> => {
|
||||
const [identity, version, peers, bandwidth, repo, bitswap] = await Promise.all([
|
||||
kuboPostJson('id', undefined, signal),
|
||||
kuboPostJson('version', undefined, signal),
|
||||
kuboPostJson('swarm/peers', { direction: true, latency: true, streams: true }, signal),
|
||||
kuboPostJson('stats/bw', undefined, signal),
|
||||
kuboPostJson('repo/stat', undefined, signal),
|
||||
kuboPostJson('bitswap/stat', undefined, signal),
|
||||
]);
|
||||
|
||||
return [
|
||||
{ name: 'mode', value: 'desktop Kubo' },
|
||||
{ name: 'PKC RPC', value: rpcState ?? 'unknown' },
|
||||
{ name: 'peer id', value: identity.ID ?? 'unknown' },
|
||||
{ name: 'agent', value: identity.AgentVersion ?? version.Version ?? 'unknown' },
|
||||
{ name: 'connected peers', value: formatCount(Array.isArray(peers.Peers) ? peers.Peers.length : 0, 'peer') },
|
||||
{ name: 'bandwidth in', value: `${formatBytes(bandwidth.TotalIn)} total, ${formatRate(bandwidth.RateIn)}` },
|
||||
{ name: 'bandwidth out', value: `${formatBytes(bandwidth.TotalOut)} total, ${formatRate(bandwidth.RateOut)}` },
|
||||
{ name: 'repo size', value: formatBytes(repo.RepoSize) },
|
||||
{ name: 'repo objects', value: String(repo.NumObjects ?? 'unknown') },
|
||||
{ name: 'bitswap peers', value: formatCount(Array.isArray(bitswap.Peers) ? bitswap.Peers.length : 0, 'peer') },
|
||||
{ name: 'bitswap wantlist', value: formatCount(Array.isArray(bitswap.Wantlist) ? bitswap.Wantlist.length : 0, 'item') },
|
||||
];
|
||||
};
|
||||
|
||||
const getP2PStats = async (mode: P2PRuntimeMode, account?: AccountShape, rpcState?: string, signal?: AbortSignal) => {
|
||||
if (mode === 'browser-libp2p') return getBrowserLibp2pStats(account);
|
||||
return getElectronKuboStats(rpcState, signal);
|
||||
};
|
||||
|
||||
const P2PStatsSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount() as AccountShape | undefined;
|
||||
const pkcRpcSettings = usePkcRpcSettings();
|
||||
const mode = getP2PRuntimeMode(account);
|
||||
const [statsState, dispatchStats] = useReducer(statsReducer, { loading: !!mode, rows: [] });
|
||||
const updatedAtLabel = statsState.updatedAt ? new Date(statsState.updatedAt).toLocaleTimeString() : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
const activeMode = mode;
|
||||
const rpcState = pkcRpcSettings?.state;
|
||||
|
||||
if (!activeMode) return () => abortController.abort();
|
||||
|
||||
const refreshStats = async () => {
|
||||
dispatchStats({ type: 'loading' });
|
||||
try {
|
||||
const rows = await getP2PStats(activeMode, account, rpcState, signal);
|
||||
if (!signal.aborted) dispatchStats({ rows, timestamp: Date.now(), type: 'loaded' });
|
||||
} catch (error) {
|
||||
if (!signal.aborted) {
|
||||
dispatchStats({
|
||||
error: getErrorMessage(error),
|
||||
timestamp: Date.now(),
|
||||
type: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void refreshStats();
|
||||
const intervalId = window.setInterval(refreshStats, STATS_REFRESH_MS);
|
||||
return () => {
|
||||
abortController.abort();
|
||||
window.clearInterval(intervalId);
|
||||
};
|
||||
}, [account, mode, pkcRpcSettings?.state]);
|
||||
|
||||
return (
|
||||
<div className={styles.content} data-testid='p2p-stats-settings-panel'>
|
||||
{mode ? (
|
||||
<>
|
||||
<table className={styles.stats}>
|
||||
<tbody>
|
||||
{statsState.rows.map((row) => (
|
||||
<tr key={row.name}>
|
||||
<td className={styles.statName}>{row.name}</td>
|
||||
<td className={styles.statValue}>{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={styles.statsMeta}>
|
||||
{statsState.loading ? t('p2p_stats_loading') : updatedAtLabel ? `${t('p2p_stats_updated')} ${updatedAtLabel}` : null}
|
||||
{statsState.error && <div className={styles.error}>{statsState.error}</div>}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.statsMeta}>{t('p2p_stats_starting')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(P2PStatsSettings);
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './settings-modal.module.css';
|
||||
@@ -9,21 +10,25 @@ import InterfaceSettings from './interface-settings';
|
||||
import MediaHostingSettings from './media-hosting-settings';
|
||||
import AdvancedSettings from './advanced-settings';
|
||||
import SubscriptionsSetting from './subscriptions-setting';
|
||||
import P2PStatsSettings from './p2p-stats-settings';
|
||||
import { P2P_STATS_SECTION_ID, shouldShowP2PSettingsSection } from '../../lib/p2p-runtime';
|
||||
|
||||
const allSectionIds = ['interface-settings', 'media-hosting-settings', 'account-settings', 'subscriptions-settings', 'advanced-settings'];
|
||||
|
||||
const hashToSection = (hash: string): string | null => {
|
||||
const hashToSection = (hash: string, sectionIds = allSectionIds): string | null => {
|
||||
if (hash === 'crypto-address-settings' || hash === 'crypto-wallet-settings') return 'account-settings';
|
||||
if (allSectionIds.includes(hash)) return hash;
|
||||
if (sectionIds.includes(hash)) return hash;
|
||||
return null;
|
||||
};
|
||||
|
||||
const SettingsModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const hash = location.hash.slice(1);
|
||||
const hashSection = hashToSection(hash);
|
||||
const sectionIds = useMemo(() => (shouldShowP2PSettingsSection(account) ? [...allSectionIds, P2P_STATS_SECTION_ID] : allSectionIds), [account]);
|
||||
const hashSection = hashToSection(hash, sectionIds);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
const newPath = location.pathname.replace(/\/settings$/, '');
|
||||
@@ -59,8 +64,9 @@ const SettingsModal = () => {
|
||||
const showAccountSettings = visibleExpandedSections.has('account-settings');
|
||||
const showSubscriptionsSettings = visibleExpandedSections.has('subscriptions-settings');
|
||||
const showAdvancedSettings = visibleExpandedSections.has('advanced-settings');
|
||||
const showP2PStatsSettings = visibleExpandedSections.has(P2P_STATS_SECTION_ID);
|
||||
|
||||
const allExpanded = useMemo(() => allSectionIds.every((id) => visibleExpandedSections.has(id)), [visibleExpandedSections]);
|
||||
const allExpanded = useMemo(() => sectionIds.every((id) => visibleExpandedSections.has(id)), [sectionIds, visibleExpandedSections]);
|
||||
|
||||
const basePath = location.pathname;
|
||||
|
||||
@@ -89,7 +95,7 @@ const SettingsModal = () => {
|
||||
setExpandedSections(new Set());
|
||||
navigate(basePath, { replace: true });
|
||||
} else {
|
||||
setExpandedSections(new Set(allSectionIds));
|
||||
setExpandedSections(new Set(sectionIds));
|
||||
navigate(basePath, { replace: true });
|
||||
}
|
||||
};
|
||||
@@ -169,6 +175,17 @@ const SettingsModal = () => {
|
||||
</label>
|
||||
</div>
|
||||
{showAdvancedSettings && <AdvancedSettings />}
|
||||
{sectionIds.includes(P2P_STATS_SECTION_ID) && (
|
||||
<>
|
||||
<div id={P2P_STATS_SECTION_ID} className={`${styles.setting} ${styles.category}`}>
|
||||
<label onClick={() => handleCategoryClick(P2P_STATS_SECTION_ID)}>
|
||||
<span className={showP2PStatsSettings ? styles.hideButton : styles.showButton} />
|
||||
{t('p2p_stats')}
|
||||
</label>
|
||||
</div>
|
||||
{showP2PStatsSettings && <P2PStatsSettings />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { configureP2PBrowserPkcOptions, isP2PBrowserHostname, P2P_BROWSER_PKC_OPTIONS } from '../p2p-browser-config';
|
||||
import {
|
||||
configureP2PBrowserPkcOptions,
|
||||
getPureP2PBrowserPreference,
|
||||
isP2PBrowserHostname,
|
||||
P2P_BROWSER_PKC_OPTIONS,
|
||||
PURE_P2P_BROWSER_SETTING_KEY,
|
||||
setPureP2PBrowserPreference,
|
||||
shouldUsePureP2PBrowser,
|
||||
} from '../p2p-browser-config';
|
||||
|
||||
const createStorage = (values: Record<string, string | undefined> = {}) => ({
|
||||
getItem: (key: string) => values[key] ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
values[key] = value;
|
||||
},
|
||||
});
|
||||
|
||||
describe('p2p-browser-config', () => {
|
||||
it('detects p2p subdomains', () => {
|
||||
@@ -10,31 +25,78 @@ describe('p2p-browser-config', () => {
|
||||
expect(isP2PBrowserHostname('www.p2p.5chan.app')).toBe(false);
|
||||
});
|
||||
|
||||
it('configures browser PKC options for p2p hostnames', () => {
|
||||
it('leaves browser PKC options untouched by default', () => {
|
||||
const targetWindow = {
|
||||
location: { hostname: 'p2p.5chan.app' },
|
||||
location: { hostname: '5chan.app' },
|
||||
localStorage: createStorage(),
|
||||
defaultPkcOptions: {
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(configureP2PBrowserPkcOptions(targetWindow)).toBe(false);
|
||||
expect(targetWindow.defaultPkcOptions).toEqual({
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
});
|
||||
});
|
||||
|
||||
it('configures browser PKC options when pure p2p is enabled', () => {
|
||||
const targetWindow = {
|
||||
location: { hostname: '5chan.app' },
|
||||
localStorage: createStorage({ [PURE_P2P_BROWSER_SETTING_KEY]: 'true' }),
|
||||
defaultPkcOptions: {
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(configureP2PBrowserPkcOptions(targetWindow)).toBe(true);
|
||||
expect(targetWindow.defaultPkcOptions).toEqual({
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
...P2P_BROWSER_PKC_OPTIONS,
|
||||
});
|
||||
expect(targetWindow.defaultPkcOptions).toEqual(P2P_BROWSER_PKC_OPTIONS);
|
||||
});
|
||||
|
||||
it('leaves normal hostnames untouched', () => {
|
||||
it('leaves browser PKC options untouched when pure p2p is disabled', () => {
|
||||
const defaultPkcOptions = {
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
};
|
||||
const targetWindow = {
|
||||
location: { hostname: '5chan.app' },
|
||||
localStorage: createStorage({ [PURE_P2P_BROWSER_SETTING_KEY]: 'false' }),
|
||||
defaultPkcOptions,
|
||||
};
|
||||
|
||||
expect(configureP2PBrowserPkcOptions(targetWindow)).toBe(false);
|
||||
expect(targetWindow.defaultPkcOptions).toBe(defaultPkcOptions);
|
||||
});
|
||||
|
||||
it('leaves electron defaults untouched', () => {
|
||||
const defaultPkcOptions = {
|
||||
pkcRpcClientsOptions: ['ws://localhost:9138'],
|
||||
};
|
||||
const targetWindow = {
|
||||
electronApi: { isElectron: true },
|
||||
location: { hostname: 'localhost' },
|
||||
localStorage: createStorage(),
|
||||
defaultPkcOptions,
|
||||
};
|
||||
|
||||
expect(configureP2PBrowserPkcOptions(targetWindow)).toBe(false);
|
||||
expect(targetWindow.defaultPkcOptions).toBe(defaultPkcOptions);
|
||||
});
|
||||
|
||||
it('persists and reads the browser pure p2p preference', () => {
|
||||
const targetWindow = {
|
||||
location: { hostname: '5chan.app' },
|
||||
localStorage: createStorage(),
|
||||
};
|
||||
|
||||
expect(getPureP2PBrowserPreference(targetWindow)).toBeUndefined();
|
||||
expect(shouldUsePureP2PBrowser(targetWindow)).toBe(false);
|
||||
|
||||
setPureP2PBrowserPreference(false, targetWindow);
|
||||
expect(getPureP2PBrowserPreference(targetWindow)).toBe(false);
|
||||
expect(shouldUsePureP2PBrowser(targetWindow)).toBe(false);
|
||||
|
||||
setPureP2PBrowserPreference(true, targetWindow);
|
||||
expect(getPureP2PBrowserPreference(targetWindow)).toBe(true);
|
||||
expect(shouldUsePureP2PBrowser(targetWindow)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
getBrowserGatewayAccountOptions,
|
||||
getBrowserPureP2PAccountOptions,
|
||||
getP2PRuntimeMode,
|
||||
isBrowserPureP2PEnabled,
|
||||
shouldShowP2PSettingsSection,
|
||||
} from '../p2p-runtime';
|
||||
|
||||
const browserWindow = {
|
||||
electronApi: undefined,
|
||||
isElectron: false,
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => undefined,
|
||||
},
|
||||
} as unknown as Window;
|
||||
|
||||
const electronWindow = {
|
||||
electronApi: { isElectron: true },
|
||||
isElectron: true,
|
||||
} as unknown as Window;
|
||||
|
||||
describe('p2p-runtime', () => {
|
||||
it('detects browser libp2p accounts from options and live clients', () => {
|
||||
expect(getP2PRuntimeMode({ pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }] } }, browserWindow)).toBe('browser-libp2p');
|
||||
expect(getP2PRuntimeMode({ pkc: { clients: { libp2pJsClients: { libp2pjs: {} } } } }, browserWindow)).toBe('browser-libp2p');
|
||||
});
|
||||
|
||||
it('detects electron Kubo RPC accounts only in electron runtime', () => {
|
||||
const account = { pkcOptions: { pkcRpcClientsOptions: ['ws://localhost:9138'] } };
|
||||
|
||||
expect(getP2PRuntimeMode(account, electronWindow)).toBe('electron-kubo-rpc');
|
||||
expect(getP2PRuntimeMode(account, browserWindow)).toBeNull();
|
||||
});
|
||||
|
||||
it('shows p2p settings in browsers only when pure p2p is enabled or active', () => {
|
||||
expect(shouldShowP2PSettingsSection(undefined, browserWindow)).toBe(false);
|
||||
expect(shouldShowP2PSettingsSection({ pkcOptions: { ipfsGatewayUrls: ['https://gateway.example'] } }, browserWindow)).toBe(false);
|
||||
expect(isBrowserPureP2PEnabled({ pkcOptions: { ipfsGatewayUrls: ['https://gateway.example'] } }, browserWindow)).toBe(false);
|
||||
});
|
||||
|
||||
it('builds browser p2p and gateway account options without a direct pkc-js import', () => {
|
||||
const account = {
|
||||
pkcOptions: {
|
||||
httpRoutersOptions: ['https://custom-router.example'],
|
||||
ipfsGatewayUrls: ['https://gateway.example'],
|
||||
pkcRpcClientsOptions: ['ws://remote.example'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(getBrowserPureP2PAccountOptions(account)).toMatchObject({
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
ipfsGatewayUrls: undefined,
|
||||
pkcRpcClientsOptions: undefined,
|
||||
});
|
||||
expect(getBrowserGatewayAccountOptions(account)).toMatchObject({
|
||||
ipfsGatewayUrls: ['https://ipfsgateway.xyz', 'https://gateway.plebpubsub.xyz', 'https://gateway.forumindex.com'],
|
||||
libp2pJsClientsOptions: undefined,
|
||||
pkcRpcClientsOptions: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,85 @@
|
||||
export const PURE_P2P_BROWSER_SETTING_KEY = '5chan:pure-p2p-browser-enabled';
|
||||
|
||||
export const P2P_BROWSER_PKC_OPTIONS = {
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
ipfsGatewayUrls: undefined,
|
||||
kuboRpcClientsOptions: undefined,
|
||||
pubsubHttpClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: undefined,
|
||||
httpRoutersOptions: ['https://peers.pleb.bot', 'https://peers.forumindex.com'],
|
||||
};
|
||||
|
||||
const GATEWAY_BROWSER_PKC_OPTIONS = {
|
||||
ipfsGatewayUrls: ['https://ipfsgateway.xyz', 'https://gateway.plebpubsub.xyz', 'https://gateway.forumindex.com'],
|
||||
kuboRpcClientsOptions: undefined,
|
||||
libp2pJsClientsOptions: undefined,
|
||||
pubsubHttpClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'],
|
||||
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
|
||||
};
|
||||
|
||||
type P2PBrowserConfigWindow = {
|
||||
location: Pick<Location, 'hostname'>;
|
||||
defaultPkcOptions?: Record<string, unknown>;
|
||||
electronApi?: { isElectron?: boolean };
|
||||
isElectron?: boolean;
|
||||
localStorage?: Pick<Storage, 'getItem' | 'setItem'>;
|
||||
};
|
||||
|
||||
export const isP2PBrowserHostname = (hostname: string) => hostname.toLowerCase().startsWith('p2p.');
|
||||
|
||||
export const getBrowserPureP2PPkcOptions = () => ({
|
||||
...P2P_BROWSER_PKC_OPTIONS,
|
||||
libp2pJsClientsOptions: P2P_BROWSER_PKC_OPTIONS.libp2pJsClientsOptions.map((options) => ({ ...options })),
|
||||
httpRoutersOptions: [...P2P_BROWSER_PKC_OPTIONS.httpRoutersOptions],
|
||||
});
|
||||
|
||||
export const getBrowserGatewayPkcOptions = () => ({
|
||||
...GATEWAY_BROWSER_PKC_OPTIONS,
|
||||
ipfsGatewayUrls: [...GATEWAY_BROWSER_PKC_OPTIONS.ipfsGatewayUrls],
|
||||
pubsubKuboRpcClientsOptions: [...GATEWAY_BROWSER_PKC_OPTIONS.pubsubKuboRpcClientsOptions],
|
||||
httpRoutersOptions: [...GATEWAY_BROWSER_PKC_OPTIONS.httpRoutersOptions],
|
||||
});
|
||||
|
||||
export const getPureP2PBrowserPreference = (targetWindow: P2PBrowserConfigWindow = window) => {
|
||||
try {
|
||||
const storedValue = targetWindow.localStorage?.getItem(PURE_P2P_BROWSER_SETTING_KEY);
|
||||
if (storedValue === 'true') return true;
|
||||
if (storedValue === 'false') return false;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const setPureP2PBrowserPreference = (enabled: boolean, targetWindow: P2PBrowserConfigWindow = window) => {
|
||||
try {
|
||||
targetWindow.localStorage?.setItem(PURE_P2P_BROWSER_SETTING_KEY, String(enabled));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const isElectronRuntime = (targetWindow: P2PBrowserConfigWindow = window) => targetWindow.electronApi?.isElectron === true || targetWindow.isElectron === true;
|
||||
|
||||
export const shouldUsePureP2PBrowser = (targetWindow: P2PBrowserConfigWindow = window) => {
|
||||
if (isElectronRuntime(targetWindow)) return false;
|
||||
|
||||
const preference = getPureP2PBrowserPreference(targetWindow);
|
||||
if (preference !== undefined) return preference;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const configureP2PBrowserPkcOptions = (targetWindow: P2PBrowserConfigWindow = window) => {
|
||||
if (!isP2PBrowserHostname(targetWindow.location.hostname)) {
|
||||
if (!shouldUsePureP2PBrowser(targetWindow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetWindow.defaultPkcOptions = {
|
||||
...targetWindow.defaultPkcOptions,
|
||||
libp2pJsClientsOptions: P2P_BROWSER_PKC_OPTIONS.libp2pJsClientsOptions.map((options) => ({ ...options })),
|
||||
httpRoutersOptions: [...P2P_BROWSER_PKC_OPTIONS.httpRoutersOptions],
|
||||
...getBrowserPureP2PPkcOptions(),
|
||||
};
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { getBrowserGatewayPkcOptions, getBrowserPureP2PPkcOptions, isElectronRuntime, shouldUsePureP2PBrowser } from './p2p-browser-config';
|
||||
|
||||
export const P2P_STATS_SECTION_ID = 'p2p-stats-settings';
|
||||
|
||||
export type P2PRuntimeMode = 'browser-libp2p' | 'electron-kubo-rpc';
|
||||
|
||||
type AccountProtocolOptions = {
|
||||
httpRoutersOptions?: string[];
|
||||
ipfsGatewayUrls?: string[];
|
||||
kuboRpcClientsOptions?: unknown[];
|
||||
libp2pJsClientsOptions?: unknown[];
|
||||
pkcRpcClientsOptions?: string[];
|
||||
pubsubHttpClientsOptions?: unknown[];
|
||||
pubsubKuboRpcClientsOptions?: unknown[];
|
||||
};
|
||||
|
||||
type AccountShape = {
|
||||
pkc?: {
|
||||
clients?: {
|
||||
libp2pJsClients?: Record<string, unknown>;
|
||||
pkcRpcClients?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
pkcOptions?: AccountProtocolOptions;
|
||||
};
|
||||
|
||||
const toAccountShape = (account: unknown) => account as AccountShape | undefined;
|
||||
|
||||
const hasArrayItems = (value: unknown) => Array.isArray(value) && value.length > 0;
|
||||
|
||||
const hasObjectItems = (value: unknown) => !!value && typeof value === 'object' && Object.keys(value).length > 0;
|
||||
|
||||
export const getP2PRuntimeMode = (account?: unknown, targetWindow: Window = window): P2PRuntimeMode | null => {
|
||||
const accountShape = toAccountShape(account);
|
||||
const protocolOptions = accountShape?.pkcOptions;
|
||||
const clients = accountShape?.pkc?.clients;
|
||||
|
||||
if (hasArrayItems(protocolOptions?.libp2pJsClientsOptions) || hasObjectItems(clients?.libp2pJsClients)) {
|
||||
return 'browser-libp2p';
|
||||
}
|
||||
|
||||
if (isElectronRuntime(targetWindow) && (hasArrayItems(protocolOptions?.pkcRpcClientsOptions) || hasObjectItems(clients?.pkcRpcClients))) {
|
||||
return 'electron-kubo-rpc';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const canConfigureBrowserPureP2P = (targetWindow: Window = window) => !isElectronRuntime(targetWindow);
|
||||
|
||||
export const shouldShowP2PSettingsSection = (account?: unknown, targetWindow: Window = window) =>
|
||||
getP2PRuntimeMode(account, targetWindow) !== null || (canConfigureBrowserPureP2P(targetWindow) && isBrowserPureP2PEnabled(account, targetWindow));
|
||||
|
||||
export const isBrowserPureP2PEnabled = (account?: unknown, targetWindow: Window = window) => {
|
||||
const accountShape = toAccountShape(account);
|
||||
if (!canConfigureBrowserPureP2P(targetWindow)) return false;
|
||||
if (getP2PRuntimeMode(account, targetWindow) === 'browser-libp2p') return true;
|
||||
if (hasArrayItems(accountShape?.pkcOptions?.ipfsGatewayUrls) || hasArrayItems(accountShape?.pkcOptions?.pubsubKuboRpcClientsOptions)) return false;
|
||||
return shouldUsePureP2PBrowser(targetWindow);
|
||||
};
|
||||
|
||||
export const getBrowserPureP2PAccountOptions = (account?: unknown) => ({
|
||||
...toAccountShape(account)?.pkcOptions,
|
||||
...getBrowserPureP2PPkcOptions(),
|
||||
pkcRpcClientsOptions: undefined,
|
||||
});
|
||||
|
||||
export const getBrowserGatewayAccountOptions = (account?: unknown) => ({
|
||||
...toAccountShape(account)?.pkcOptions,
|
||||
...getBrowserGatewayPkcOptions(),
|
||||
pkcRpcClientsOptions: undefined,
|
||||
});
|
||||
@@ -592,18 +592,13 @@ const FAQ_SECTIONS: FAQSection[] = [
|
||||
question: 'Can browsers connect peer-to-peer?',
|
||||
answer: (
|
||||
<>
|
||||
Yes, experimentally. The standard web app can use HTTP gateways, routers, and pubsub providers as compatibility helpers. The{' '}
|
||||
<a href='https://p2p.5chan.app' {...externalLinkProps}>
|
||||
p2p.5chan.app
|
||||
</a>{' '}
|
||||
subdomain instead runs a{' '}
|
||||
Yes. The web app can run a{' '}
|
||||
<a href='https://helia.io' {...externalLinkProps}>
|
||||
Helia
|
||||
</a>{' '}
|
||||
IPFS node in the browser, so it can load boards peer-to-peer without centralized IPFS RPC gateways. Browser nodes still have restrictions, such as limited
|
||||
inbound connectivity, so they can load data peer-to-peer but are not a stable way to host board data yet. Helia pubsub is currently unstable for 5chan's
|
||||
needs, which is why the pure browser P2P site is experimental. The desktop apps are the stable pure-P2P option today because they run a full Bitsocial node
|
||||
with IPFS Kubo.
|
||||
IPFS node in the browser, so it can load boards peer-to-peer without centralized IPFS RPC gateways. You can turn pure browser P2P on in advanced settings and
|
||||
inspect the connection in <Link to='/all/settings#p2p-stats-settings'>P2P stats</Link>. Browser nodes still have restrictions, such as limited inbound
|
||||
connectivity, so desktop apps remain the best way to host board data because they run a full Bitsocial node with IPFS Kubo.
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "5chan@workspace:."
|
||||
dependencies:
|
||||
"@bitsocial/bitsocial-react-hooks": "npm:0.1.6"
|
||||
"@bitsocial/bitsocial-react-hooks": "npm:0.1.8"
|
||||
"@capacitor/android": "npm:7.4.5"
|
||||
"@capacitor/app": "npm:7.0.1"
|
||||
"@capacitor/cli": "npm:7.4.5"
|
||||
@@ -23,7 +23,7 @@ __metadata:
|
||||
"@electron-forge/maker-zip": "npm:7.8.0"
|
||||
"@electron/rebuild": "npm:3.7.2"
|
||||
"@floating-ui/react": "npm:0.26.1"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.23"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.29"
|
||||
"@react-spring/web": "npm:10.0.3"
|
||||
"@reforged/maker-appimage": "npm:5.1.1"
|
||||
"@types/lodash": "npm:4.17.24"
|
||||
@@ -1522,12 +1522,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bitsocial/bitsocial-react-hooks@npm:0.1.6":
|
||||
version: 0.1.6
|
||||
resolution: "@bitsocial/bitsocial-react-hooks@npm:0.1.6"
|
||||
"@bitsocial/bitsocial-react-hooks@npm:0.1.8":
|
||||
version: 0.1.8
|
||||
resolution: "@bitsocial/bitsocial-react-hooks@npm:0.1.8"
|
||||
dependencies:
|
||||
"@bitsocial/bso-resolver": "npm:0.0.6"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.24"
|
||||
"@bitsocial/bso-resolver": "npm:0.0.8"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.29"
|
||||
"@pkcprotocol/pkc-logger": "npm:0.1.0"
|
||||
assert: "npm:2.0.0"
|
||||
ethers: "npm:5.8.0"
|
||||
@@ -1543,18 +1543,17 @@ __metadata:
|
||||
zustand: "npm:4.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8"
|
||||
checksum: 10c0/7c93c9430303ed60f32d2eff8601b700ea103f7d801e831967a8f7d34a4876f1087eeaf9bb0250ff36008a7276ad7146934713c3c5f52411faa46219e31576bd
|
||||
checksum: 10c0/0222c68180ff30372b1ff415bc4c47c47bf1de2ef74bd08457730922498d0eff6ce42b379f96ebefd32a18f37cddff5e2431a7d338634efa7fb6e404e1fc9c2a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bitsocial/bso-resolver@npm:0.0.6":
|
||||
version: 0.0.6
|
||||
resolution: "@bitsocial/bso-resolver@npm:0.0.6"
|
||||
"@bitsocial/bso-resolver@npm:0.0.8":
|
||||
version: 0.0.8
|
||||
resolution: "@bitsocial/bso-resolver@npm:0.0.8"
|
||||
dependencies:
|
||||
"@pkcprotocol/pkc-logger": "npm:0.1.0"
|
||||
better-sqlite3: "npm:12.6.2"
|
||||
viem: "npm:2.48.1"
|
||||
checksum: 10c0/113e54c08dd86c1e356318e6be1b2b1682753209bba229256352a47bbaef81d340a46c5124f4abf8f49d1289caae479637e902b45ac33e950fc05a34caf0e044
|
||||
checksum: 10c0/0acb033cac4f8860323e154a5a53b95e9d107326e78bc9febd36091cd96dc2c095b31996d594fba9d676c132305c5504d5b7e1b3e5af1f78e2c372f1eebc2f7a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3984,28 +3983,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@libp2p/gossipsub@npm:15.0.20":
|
||||
version: 15.0.20
|
||||
resolution: "@libp2p/gossipsub@npm:15.0.20"
|
||||
dependencies:
|
||||
"@libp2p/crypto": "npm:^5.1.17"
|
||||
"@libp2p/interface": "npm:^3.2.2"
|
||||
"@libp2p/interface-internal": "npm:^3.1.3"
|
||||
"@libp2p/peer-id": "npm:^6.0.8"
|
||||
"@libp2p/utils": "npm:^7.1.0"
|
||||
"@multiformats/multiaddr": "npm:^13.0.1"
|
||||
denque: "npm:^2.1.0"
|
||||
it-length-prefixed: "npm:^10.0.1"
|
||||
it-pipe: "npm:^3.0.1"
|
||||
it-pushable: "npm:^3.2.3"
|
||||
multiformats: "npm:^13.0.1"
|
||||
protons-runtime: "npm:^6.0.1"
|
||||
uint8arraylist: "npm:^2.4.8"
|
||||
uint8arrays: "npm:^5.0.1"
|
||||
checksum: 10c0/0b96b09e3f57768bc7a281a7c043e27fbd1e8cafd03c274a6fc5e33c0391c11f0a1a393e5f4accd7b2c11b822bb430cbcf8590d71e50afa2fce2dae8866ea41f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@libp2p/gossipsub@npm:15.0.21":
|
||||
version: 15.0.21
|
||||
resolution: "@libp2p/gossipsub@npm:15.0.21"
|
||||
@@ -4621,7 +4598,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@libp2p/websockets@npm:10.1.11, @libp2p/websockets@npm:^10.1.8":
|
||||
"@libp2p/websockets@npm:^10.1.8":
|
||||
version: 10.1.11
|
||||
resolution: "@libp2p/websockets@npm:10.1.11"
|
||||
dependencies:
|
||||
@@ -4640,6 +4617,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@libp2p/webtransport@npm:6.0.22":
|
||||
version: 6.0.22
|
||||
resolution: "@libp2p/webtransport@npm:6.0.22"
|
||||
dependencies:
|
||||
"@chainsafe/libp2p-noise": "npm:^17.0.0"
|
||||
"@libp2p/interface": "npm:^3.2.2"
|
||||
"@libp2p/peer-id": "npm:^6.0.8"
|
||||
"@libp2p/utils": "npm:^7.1.0"
|
||||
"@multiformats/multiaddr": "npm:^13.0.1"
|
||||
"@multiformats/multiaddr-matcher": "npm:^3.0.1"
|
||||
multiformats: "npm:^13.3.6"
|
||||
progress-events: "npm:^1.0.1"
|
||||
race-signal: "npm:^2.0.0"
|
||||
uint8arraylist: "npm:^2.4.8"
|
||||
uint8arrays: "npm:^5.1.0"
|
||||
checksum: 10c0/152aaf9b2895681b05b243b5fa156eb4c510d655b9880ac105c8963741dae2690e7eef079a1484569e22745a065ea9580ed543121db039ed77327fb715544b32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@malept/cross-spawn-promise@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@malept/cross-spawn-promise@npm:2.0.0"
|
||||
@@ -5759,69 +5755,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pkcprotocol/pkc-js@npm:0.0.23":
|
||||
version: 0.0.23
|
||||
resolution: "@pkcprotocol/pkc-js@npm:0.0.23"
|
||||
dependencies:
|
||||
"@enhances/with-resolvers": "npm:0.0.5"
|
||||
"@helia/block-brokers": "npm:5.2.4"
|
||||
"@helia/delegated-routing-v1-http-api-client": "npm:6.0.1"
|
||||
"@helia/ipns": "npm:9.2.1"
|
||||
"@helia/unixfs": "npm:7.2.1"
|
||||
"@libp2p/crypto": "npm:5.1.17"
|
||||
"@libp2p/fetch": "npm:4.1.3"
|
||||
"@libp2p/gossipsub": "npm:15.0.20"
|
||||
"@libp2p/identify": "npm:4.1.3"
|
||||
"@libp2p/interfaces": "npm:3.3.2"
|
||||
"@libp2p/peer-id": "npm:6.0.8"
|
||||
"@libp2p/websockets": "npm:10.1.11"
|
||||
"@noble/curves": "npm:2.2.0"
|
||||
"@pkcprotocol/pkc-logger": "npm:0.1.0"
|
||||
"@pkcprotocol/proper-lock-file": "npm:4.2.1"
|
||||
assert: "npm:2.1.0"
|
||||
better-sqlite3: "npm:12.9.0"
|
||||
blockstore-core: "npm:6.1.2"
|
||||
blockstore-fs: "npm:3.0.2"
|
||||
blockstore-idb: "npm:3.0.1"
|
||||
buffer: "npm:6.0.3"
|
||||
cbor: "npm:10.0.11"
|
||||
debounce: "npm:1.2.1"
|
||||
err-code: "npm:3.0.1"
|
||||
ext-name: "npm:5.0.0"
|
||||
helia: "npm:6.1.4"
|
||||
hpagent: "npm:1.2.0"
|
||||
it-all: "npm:3.0.6"
|
||||
js-sha256: "npm:0.11.1"
|
||||
js-sha512: "npm:0.9.0"
|
||||
kubo-rpc-client: "npm:6.1.0"
|
||||
limiter-es6-compat: "npm:2.1.2"
|
||||
localforage: "npm:1.10.0"
|
||||
lodash.merge: "npm:4.6.2"
|
||||
lru-cache: "npm:10.1.0"
|
||||
open-graph-scraper: "npm:6.11.0"
|
||||
p-limit: "npm:7.3.0"
|
||||
p-retry: "npm:8.0.0"
|
||||
p-timeout: "npm:7.0.1"
|
||||
probe-image-size: "npm:7.2.3"
|
||||
remeda: "npm:1.57.0"
|
||||
retry: "npm:0.13.1"
|
||||
rpc-websockets: "npm:9.3.8"
|
||||
safe-stable-stringify: "npm:2.4.3"
|
||||
sha1-uint8array: "npm:0.10.7"
|
||||
tcp-port-used: "npm:1.0.2"
|
||||
tiny-typed-emitter: "npm:2.1.0"
|
||||
tinycache: "npm:1.1.2"
|
||||
ts-custom-error: "npm:3.3.1"
|
||||
typestub-ipfs-only-hash: "npm:4.0.0"
|
||||
uuid: "npm:13.0.0"
|
||||
zod: "npm:4.3.6"
|
||||
checksum: 10c0/c575983ca7d7fadff4f9058d700e0df210159ee086c4b44b31e68f1653f9c429766c12b440b1ab5d3b82dfe1a2807be1b6d7732379d64d5c6694a0f85f0b28f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pkcprotocol/pkc-js@npm:0.0.24":
|
||||
version: 0.0.24
|
||||
resolution: "@pkcprotocol/pkc-js@npm:0.0.24"
|
||||
"@pkcprotocol/pkc-js@npm:0.0.29":
|
||||
version: 0.0.29
|
||||
resolution: "@pkcprotocol/pkc-js@npm:0.0.29"
|
||||
dependencies:
|
||||
"@enhances/with-resolvers": "npm:0.0.5"
|
||||
"@helia/block-brokers": "npm:5.2.4"
|
||||
@@ -5834,19 +5770,16 @@ __metadata:
|
||||
"@libp2p/identify": "npm:4.1.3"
|
||||
"@libp2p/interfaces": "npm:3.3.2"
|
||||
"@libp2p/peer-id": "npm:6.0.8"
|
||||
"@libp2p/websockets": "npm:10.1.11"
|
||||
"@libp2p/webtransport": "npm:6.0.22"
|
||||
"@noble/curves": "npm:2.2.0"
|
||||
"@pkcprotocol/pkc-logger": "npm:0.1.0"
|
||||
"@pkcprotocol/proper-lock-file": "npm:4.2.1"
|
||||
assert: "npm:2.1.0"
|
||||
better-sqlite3: "npm:12.9.0"
|
||||
blockstore-core: "npm:6.1.2"
|
||||
blockstore-fs: "npm:3.0.2"
|
||||
blockstore-idb: "npm:3.0.1"
|
||||
buffer: "npm:6.0.3"
|
||||
cbor: "npm:10.0.11"
|
||||
debounce: "npm:1.2.1"
|
||||
err-code: "npm:3.0.1"
|
||||
ext-name: "npm:5.0.0"
|
||||
helia: "npm:6.1.4"
|
||||
hpagent: "npm:1.2.0"
|
||||
@@ -5867,7 +5800,6 @@ __metadata:
|
||||
retry: "npm:0.13.1"
|
||||
rpc-websockets: "npm:9.3.8"
|
||||
safe-stable-stringify: "npm:2.4.3"
|
||||
sha1-uint8array: "npm:0.10.7"
|
||||
tcp-port-used: "npm:1.0.2"
|
||||
tiny-typed-emitter: "npm:2.1.0"
|
||||
tinycache: "npm:1.1.2"
|
||||
@@ -5875,7 +5807,7 @@ __metadata:
|
||||
typestub-ipfs-only-hash: "npm:4.0.0"
|
||||
uuid: "npm:13.0.0"
|
||||
zod: "npm:4.3.6"
|
||||
checksum: 10c0/8acff68a6ce5f1d545ce0a2f47e4a67eb9cc166d37fb9f13c8b3582b2124f984317b68a3ed4e8b7d386c19789f59a1acce8ac1284b40b18715b12a4ba488d7f8
|
||||
checksum: 10c0/089100e75d4b2c735f3a9b9f6b087970afb9dc606e21be6e5e1e84daf2e3350793f5cfa8f6508a3db7bc518a1173e517f449174185b2a07f5df7d33f73cffdac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7504,17 +7436,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"better-sqlite3@npm:12.6.2":
|
||||
version: 12.6.2
|
||||
resolution: "better-sqlite3@npm:12.6.2"
|
||||
dependencies:
|
||||
bindings: "npm:^1.5.0"
|
||||
node-gyp: "npm:latest"
|
||||
prebuild-install: "npm:^7.1.1"
|
||||
checksum: 10c0/a58fb3f7a7f5469ba0b8de0855aa67396ff34f951a6975746e4b21987f530be6a34427d1d4bd5958cf48c67ed7ba1df038ae163d2ee9d944237f6b8112f6640d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"better-sqlite3@npm:12.9.0":
|
||||
version: 12.9.0
|
||||
resolution: "better-sqlite3@npm:12.9.0"
|
||||
@@ -7619,7 +7540,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"blockstore-core@npm:6.1.2, blockstore-core@npm:^6.0.0, blockstore-core@npm:^6.1.2":
|
||||
"blockstore-core@npm:6.1.2, blockstore-core@npm:^6.1.2":
|
||||
version: 6.1.2
|
||||
resolution: "blockstore-core@npm:6.1.2"
|
||||
dependencies:
|
||||
@@ -7649,38 +7570,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"blockstore-fs@npm:3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "blockstore-fs@npm:3.0.2"
|
||||
dependencies:
|
||||
interface-blockstore: "npm:^6.0.0"
|
||||
interface-store: "npm:^7.0.0"
|
||||
it-glob: "npm:^3.0.3"
|
||||
it-map: "npm:^3.1.3"
|
||||
it-parallel-batch: "npm:^3.0.8"
|
||||
multiformats: "npm:^13.3.6"
|
||||
race-signal: "npm:^2.0.0"
|
||||
steno: "npm:^4.0.2"
|
||||
checksum: 10c0/326707456edd7296be495f195a25ab29f92554d07f2cddb03d8648f6f2ea3d2506f06350ae7a4b68938bc930d1a389acea28cdfe9a31db9379d1e23983d598d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"blockstore-idb@npm:3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "blockstore-idb@npm:3.0.1"
|
||||
dependencies:
|
||||
blockstore-core: "npm:^6.0.0"
|
||||
idb: "npm:^8.0.3"
|
||||
interface-blockstore: "npm:^6.0.0"
|
||||
interface-store: "npm:^7.0.0"
|
||||
it-all: "npm:^3.0.9"
|
||||
it-to-buffer: "npm:^4.0.10"
|
||||
multiformats: "npm:^13.3.6"
|
||||
race-signal: "npm:^2.0.0"
|
||||
checksum: 10c0/d2b63f13a120e7aa167cbd64fe2d9e30ec27e2f57bf37455c9fdbe9b25218a4c65a5be7c2139e848ca21ece2cace7116aeaf67ceef0b34971fd6e0c98aa52696
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bluebird@npm:^3.1.1":
|
||||
version: 3.7.2
|
||||
resolution: "bluebird@npm:3.7.2"
|
||||
@@ -9747,13 +9636,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"err-code@npm:3.0.1, err-code@npm:^3.0.0, err-code@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "err-code@npm:3.0.1"
|
||||
checksum: 10c0/78b1c50500adebde6699b8d27b8ce4728c132dcaad75b5d18ba44f6ccb28769d1fff8368ae1164be4559dac8b95d4e26bb15b480ba9999e0cd0f0c64beaf1b24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"err-code@npm:^2.0.2":
|
||||
version: 2.0.3
|
||||
resolution: "err-code@npm:2.0.3"
|
||||
@@ -9761,6 +9643,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"err-code@npm:^3.0.0, err-code@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "err-code@npm:3.0.1"
|
||||
checksum: 10c0/78b1c50500adebde6699b8d27b8ce4728c132dcaad75b5d18ba44f6ccb28769d1fff8368ae1164be4559dac8b95d4e26bb15b480ba9999e0cd0f0c64beaf1b24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"error-ex@npm:^1.2.0, error-ex@npm:^1.3.1, error-ex@npm:^1.3.2":
|
||||
version: 1.3.4
|
||||
resolution: "error-ex@npm:1.3.4"
|
||||
@@ -11801,13 +11690,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"idb@npm:^8.0.3":
|
||||
version: 8.0.3
|
||||
resolution: "idb@npm:8.0.3"
|
||||
checksum: 10c0/421cd9a3281b7564528857031cc33fd9e95753f8191e483054cb25d1ceea7303a0d1462f4f69f5b41606f0f066156999e067478abf2460dfcf9cab80dae2a2b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
@@ -12941,7 +12823,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"it-glob@npm:^3.0.1, it-glob@npm:^3.0.3":
|
||||
"it-glob@npm:^3.0.1":
|
||||
version: 3.0.4
|
||||
resolution: "it-glob@npm:3.0.4"
|
||||
dependencies:
|
||||
@@ -13060,7 +12942,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"it-parallel-batch@npm:^3.0.8, it-parallel-batch@npm:^3.0.9":
|
||||
"it-parallel-batch@npm:^3.0.9":
|
||||
version: 3.0.9
|
||||
resolution: "it-parallel-batch@npm:3.0.9"
|
||||
dependencies:
|
||||
@@ -17798,13 +17680,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sha1-uint8array@npm:0.10.7":
|
||||
version: 0.10.7
|
||||
resolution: "sha1-uint8array@npm:0.10.7"
|
||||
checksum: 10c0/7c2a62aef9c6b8fded4df84d8356038895c8e2a87eedbfb88ea5d74b9e12502ac7c457a3f0708424c5f8f9ce32f040bb89399b352bc772c99f9c0e4e89dddf57
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sharp@npm:0.34.5":
|
||||
version: 0.34.5
|
||||
resolution: "sharp@npm:0.34.5"
|
||||
@@ -18296,13 +18171,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"steno@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "steno@npm:4.0.2"
|
||||
checksum: 10c0/fda6bb192bb73ce9453586080aae3fc75506e1c10c9285df49849a016bb9378c6146610f27976708437a6c5492619ec09efb9def6d5397624ae0ad70e57ccaf1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stop-iteration-iterator@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "stop-iteration-iterator@npm:1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user