mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/feature/p2p-stats-settings'
# Conflicts: # package.json # public/translations/ar/default.json # public/translations/bn/default.json # public/translations/cs/default.json # public/translations/da/default.json # public/translations/de/default.json # public/translations/el/default.json # public/translations/en/default.json # public/translations/es/default.json # public/translations/fa/default.json # public/translations/fi/default.json # public/translations/fil/default.json # public/translations/fr/default.json # public/translations/he/default.json # public/translations/hi/default.json # public/translations/hu/default.json # public/translations/id/default.json # public/translations/it/default.json # public/translations/ja/default.json # public/translations/ko/default.json # public/translations/mr/default.json # public/translations/nl/default.json # public/translations/no/default.json # public/translations/pl/default.json # public/translations/pt/default.json # public/translations/ro/default.json # public/translations/ru/default.json # public/translations/sq/default.json # public/translations/sv/default.json # public/translations/te/default.json # public/translations/th/default.json # public/translations/tr/default.json # public/translations/uk/default.json # public/translations/ur/default.json # public/translations/vi/default.json # public/translations/zh/default.json # src/components/settings-modal/__tests__/settings-modal.test.tsx # src/components/settings-modal/settings-modal.tsx # yarn.lock
This commit is contained in:
+2
-2
@@ -8,14 +8,14 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@bitsocial/bitsocial-react-hooks": "0.1.7",
|
||||
"@bitsocial/bitsocial-react-hooks": "0.1.8",
|
||||
"@capacitor/app": "7.0.1",
|
||||
"@capacitor/browser": "7.0.5",
|
||||
"@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",
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "لا توجد روابط لوحات موثوقة.",
|
||||
"trusted_board_link_until_revoked": "حتى يتم الإلغاء",
|
||||
"trusted_board_link_checkbox": "اسمح دائمًا للوحات بفتح هذا الموقع الإلكتروني",
|
||||
"revoke": "إلغاء"
|
||||
"revoke": "إلغاء",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "কোনো বিশ্বস্ত বোর্ড লিংক নেই।",
|
||||
"trusted_board_link_until_revoked": "প্রত্যাহার না করা পর্যন্ত",
|
||||
"trusted_board_link_checkbox": "বোর্ডগুলোকে সবসময় এই ওয়েবসাইট খুলতে দিন",
|
||||
"revoke": "প্রত্যাহার করুন"
|
||||
"revoke": "প্রত্যাহার করুন",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Žádné důvěryhodné odkazy desek.",
|
||||
"trusted_board_link_until_revoked": "dokud nebude odvoláno",
|
||||
"trusted_board_link_checkbox": "Vždy povolit deskám otevírat tento web",
|
||||
"revoke": "Odvolat"
|
||||
"revoke": "Odvolat",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Ingen betroede tavlelinks.",
|
||||
"trusted_board_link_until_revoked": "indtil tilbagekaldt",
|
||||
"trusted_board_link_checkbox": "Tillad altid tavler at åbne dette websted",
|
||||
"revoke": "Tilbagekald"
|
||||
"revoke": "Tilbagekald",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Keine vertrauenswürdigen Board-Links.",
|
||||
"trusted_board_link_until_revoked": "bis zum Widerruf",
|
||||
"trusted_board_link_checkbox": "Boards immer erlauben, diese Website zu öffnen",
|
||||
"revoke": "Widerrufen"
|
||||
"revoke": "Widerrufen",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Δεν υπάρχουν αξιόπιστοι σύνδεσμοι πινάκων.",
|
||||
"trusted_board_link_until_revoked": "μέχρι να ανακληθεί",
|
||||
"trusted_board_link_checkbox": "Να επιτρέπεται πάντα στους πίνακες να ανοίγουν αυτόν τον ιστότοπο",
|
||||
"revoke": "Ανάκληση"
|
||||
"revoke": "Ανάκληση",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "No trusted board links.",
|
||||
"trusted_board_link_until_revoked": "until revoked",
|
||||
"trusted_board_link_checkbox": "Always allow boards to open this website",
|
||||
"revoke": "Revoke"
|
||||
"revoke": "Revoke",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "No hay enlaces de tableros de confianza.",
|
||||
"trusted_board_link_until_revoked": "hasta que se revoque",
|
||||
"trusted_board_link_checkbox": "Permitir siempre que los tableros abran este sitio web",
|
||||
"revoke": "Revocar"
|
||||
"revoke": "Revocar",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "هیچ پیوند برد مورد اعتمادی وجود ندارد.",
|
||||
"trusted_board_link_until_revoked": "تا زمان لغو",
|
||||
"trusted_board_link_checkbox": "همیشه به بردها اجازه دهید این وبسایت را باز کنند",
|
||||
"revoke": "لغو"
|
||||
"revoke": "لغو",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Ei luotettuja taululinkkejä.",
|
||||
"trusted_board_link_until_revoked": "kunnes perutaan",
|
||||
"trusted_board_link_checkbox": "Salli taulujen aina avata tämä verkkosivusto",
|
||||
"revoke": "Peruuta"
|
||||
"revoke": "Peruuta",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Walang pinagkakatiwalaang link ng board.",
|
||||
"trusted_board_link_until_revoked": "hanggang bawiin",
|
||||
"trusted_board_link_checkbox": "Palaging payagan ang mga board na buksan ang website na ito",
|
||||
"revoke": "Bawiin"
|
||||
"revoke": "Bawiin",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Aucun lien de board approuvé.",
|
||||
"trusted_board_link_until_revoked": "jusqu'à révocation",
|
||||
"trusted_board_link_checkbox": "Toujours autoriser les boards à ouvrir ce site web",
|
||||
"revoke": "Révoquer"
|
||||
"revoke": "Révoquer",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "אין קישורי לוחות מהימנים.",
|
||||
"trusted_board_link_until_revoked": "עד לביטול",
|
||||
"trusted_board_link_checkbox": "לאפשר תמיד ללוחות לפתוח את האתר הזה",
|
||||
"revoke": "בטל"
|
||||
"revoke": "בטל",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "कोई विश्वसनीय बोर्ड लिंक नहीं।",
|
||||
"trusted_board_link_until_revoked": "रद्द होने तक",
|
||||
"trusted_board_link_checkbox": "बोर्ड को हमेशा यह वेबसाइट खोलने दें",
|
||||
"revoke": "रद्द करें"
|
||||
"revoke": "रद्द करें",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Nincsenek megbízható táblahivatkozások.",
|
||||
"trusted_board_link_until_revoked": "visszavonásig",
|
||||
"trusted_board_link_checkbox": "Mindig engedélyezze a tábláknak ennek a webhelynek a megnyitását",
|
||||
"revoke": "Visszavonás"
|
||||
"revoke": "Visszavonás",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Tidak ada tautan papan tepercaya.",
|
||||
"trusted_board_link_until_revoked": "hingga dicabut",
|
||||
"trusted_board_link_checkbox": "Selalu izinkan papan membuka situs web ini",
|
||||
"revoke": "Cabut"
|
||||
"revoke": "Cabut",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Nessun link di bacheca attendibile.",
|
||||
"trusted_board_link_until_revoked": "fino alla revoca",
|
||||
"trusted_board_link_checkbox": "Consenti sempre alle bacheche di aprire questo sito web",
|
||||
"revoke": "Revoca"
|
||||
"revoke": "Revoca",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "信頼済みの板リンクはありません。",
|
||||
"trusted_board_link_until_revoked": "取り消すまで",
|
||||
"trusted_board_link_checkbox": "板が常にこのウェブサイトを開くことを許可する",
|
||||
"revoke": "取り消す"
|
||||
"revoke": "取り消す",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "신뢰한 게시판 링크가 없습니다.",
|
||||
"trusted_board_link_until_revoked": "취소할 때까지",
|
||||
"trusted_board_link_checkbox": "게시판이 항상 이 웹사이트를 열도록 허용",
|
||||
"revoke": "취소"
|
||||
"revoke": "취소",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "विश्वसनीय बोर्ड दुवे नाहीत.",
|
||||
"trusted_board_link_until_revoked": "रद्द करेपर्यंत",
|
||||
"trusted_board_link_checkbox": "बोर्डना नेहमी ही वेबसाइट उघडण्याची परवानगी द्या",
|
||||
"revoke": "रद्द करा"
|
||||
"revoke": "रद्द करा",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Geen vertrouwde boardlinks.",
|
||||
"trusted_board_link_until_revoked": "tot intrekking",
|
||||
"trusted_board_link_checkbox": "Sta boards altijd toe deze website te openen",
|
||||
"revoke": "Intrekken"
|
||||
"revoke": "Intrekken",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Ingen klarerte tavlelenker.",
|
||||
"trusted_board_link_until_revoked": "til den tilbakekalles",
|
||||
"trusted_board_link_checkbox": "Tillat alltid tavler å åpne dette nettstedet",
|
||||
"revoke": "Tilbakekall"
|
||||
"revoke": "Tilbakekall",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Brak zaufanych linków tablic.",
|
||||
"trusted_board_link_until_revoked": "do odwołania",
|
||||
"trusted_board_link_checkbox": "Zawsze pozwalaj tablicom otwierać tę stronę internetową",
|
||||
"revoke": "Odwołaj"
|
||||
"revoke": "Odwołaj",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Nenhum link de board confiável.",
|
||||
"trusted_board_link_until_revoked": "até ser revogado",
|
||||
"trusted_board_link_checkbox": "Sempre permitir que boards abram este site",
|
||||
"revoke": "Revogar"
|
||||
"revoke": "Revogar",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Nu există linkuri de board de încredere.",
|
||||
"trusted_board_link_until_revoked": "până la revocare",
|
||||
"trusted_board_link_checkbox": "Permite mereu boardurilor să deschidă acest site web",
|
||||
"revoke": "Revocă"
|
||||
"revoke": "Revocă",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Нет доверенных ссылок досок.",
|
||||
"trusted_board_link_until_revoked": "до отзыва",
|
||||
"trusted_board_link_checkbox": "Всегда разрешать доскам открывать этот сайт",
|
||||
"revoke": "Отозвать"
|
||||
"revoke": "Отозвать",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Nuk ka lidhje bordesh të besuara.",
|
||||
"trusted_board_link_until_revoked": "derisa të revokohet",
|
||||
"trusted_board_link_checkbox": "Lejo gjithmonë bordet të hapin këtë faqe interneti",
|
||||
"revoke": "Revoko"
|
||||
"revoke": "Revoko",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Inga betrodda boardlänkar.",
|
||||
"trusted_board_link_until_revoked": "tills den återkallas",
|
||||
"trusted_board_link_checkbox": "Tillåt alltid boards att öppna den här webbplatsen",
|
||||
"revoke": "Återkalla"
|
||||
"revoke": "Återkalla",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "నమ్మదగిన బోర్డ్ లింకులు లేవు.",
|
||||
"trusted_board_link_until_revoked": "రద్దు చేసే వరకు",
|
||||
"trusted_board_link_checkbox": "బోర్డులు ఎల్లప్పుడూ ఈ వెబ్సైట్ను తెరవడానికి అనుమతించు",
|
||||
"revoke": "రద్దు చేయి"
|
||||
"revoke": "రద్దు చేయి",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "ไม่มีลิงก์บอร์ดที่เชื่อถือได้",
|
||||
"trusted_board_link_until_revoked": "จนกว่าจะเพิกถอน",
|
||||
"trusted_board_link_checkbox": "อนุญาตให้บอร์ดเปิดเว็บไซต์นี้เสมอ",
|
||||
"revoke": "เพิกถอน"
|
||||
"revoke": "เพิกถอน",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Güvenilir pano bağlantısı yok.",
|
||||
"trusted_board_link_until_revoked": "iptal edilene kadar",
|
||||
"trusted_board_link_checkbox": "Panoların bu web sitesini her zaman açmasına izin ver",
|
||||
"revoke": "İptal et"
|
||||
"revoke": "İptal et",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Немає довірених посилань дощок.",
|
||||
"trusted_board_link_until_revoked": "до відкликання",
|
||||
"trusted_board_link_checkbox": "Завжди дозволяти дошкам відкривати цей сайт",
|
||||
"revoke": "Відкликати"
|
||||
"revoke": "Відкликати",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "کوئی قابل اعتماد بورڈ لنکس نہیں۔",
|
||||
"trusted_board_link_until_revoked": "منسوخ ہونے تک",
|
||||
"trusted_board_link_checkbox": "بورڈز کو ہمیشہ یہ ویب سائٹ کھولنے کی اجازت دیں",
|
||||
"revoke": "منسوخ کریں"
|
||||
"revoke": "منسوخ کریں",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "Không có liên kết bảng đáng tin cậy.",
|
||||
"trusted_board_link_until_revoked": "cho đến khi thu hồi",
|
||||
"trusted_board_link_checkbox": "Luôn cho phép các bảng mở trang web này",
|
||||
"revoke": "Thu hồi"
|
||||
"revoke": "Thu hồi",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -371,5 +371,11 @@
|
||||
"trusted_board_links_empty": "没有受信任的版块链接。",
|
||||
"trusted_board_link_until_revoked": "直到撤销",
|
||||
"trusted_board_link_checkbox": "始终允许版块打开此网站",
|
||||
"revoke": "撤销"
|
||||
"revoke": "撤销",
|
||||
"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,
|
||||
@@ -46,6 +58,10 @@ vi.mock('../trusted-board-links-setting', () => ({
|
||||
default: () => <div data-testid='trusted-board-links-settings-panel'>trusted-board-links-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>;
|
||||
@@ -139,6 +155,7 @@ describe('SettingsModal', () => {
|
||||
expect(container.querySelector('[data-testid="subscriptions-settings-panel"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="trusted-board-links-settings-panel"]')).not.toBeNull();
|
||||
expect(container.querySelector('[data-testid="advanced-settings-panel"]')).not.toBeNull();
|
||||
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'),
|
||||
@@ -157,6 +174,13 @@ describe('SettingsModal', () => {
|
||||
expect(container.querySelector('[data-testid="subscriptions-settings-panel"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="trusted-board-links-settings-panel"]')).toBeNull();
|
||||
expect(container.querySelector('[data-testid="advanced-settings-panel"]')).toBeNull();
|
||||
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: {
|
||||
@@ -191,6 +192,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 {
|
||||
@@ -11,6 +13,7 @@ interface SettingsProps {
|
||||
ethRpcRef?: RefObject<HTMLTextAreaElement>;
|
||||
p2pRpcRef?: RefObject<HTMLInputElement>;
|
||||
p2pDataPathRef?: RefObject<HTMLInputElement>;
|
||||
pureP2PBrowserRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
type AccountProtocolOptions = {
|
||||
@@ -18,6 +21,8 @@ type AccountProtocolOptions = {
|
||||
dataPath?: string;
|
||||
httpRoutersOptions?: string[];
|
||||
ipfsGatewayUrls?: string[];
|
||||
kuboRpcClientsOptions?: unknown[];
|
||||
libp2pJsClientsOptions?: unknown[];
|
||||
pkcRpcClientsOptions?: string[];
|
||||
pubsubHttpClientsOptions?: string[];
|
||||
pubsubKuboRpcClientsOptions?: string[];
|
||||
@@ -197,6 +202,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 = () => {
|
||||
@@ -211,6 +231,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
|
||||
@@ -237,26 +258,51 @@ 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) {
|
||||
chainProviders.eth = { urls: ethRpcUrls, chainId: 1 };
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -271,6 +317,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';
|
||||
@@ -10,6 +11,8 @@ import MediaHostingSettings from './media-hosting-settings';
|
||||
import AdvancedSettings from './advanced-settings';
|
||||
import SubscriptionsSetting from './subscriptions-setting';
|
||||
import TrustedBoardLinksSetting from './trusted-board-links-setting';
|
||||
import P2PStatsSettings from './p2p-stats-settings';
|
||||
import { P2P_STATS_SECTION_ID, shouldShowP2PSettingsSection } from '../../lib/p2p-runtime';
|
||||
|
||||
const allSectionIds = [
|
||||
'interface-settings',
|
||||
@@ -20,18 +23,20 @@ const allSectionIds = [
|
||||
'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$/, '');
|
||||
@@ -68,8 +73,9 @@ const SettingsModal = () => {
|
||||
const showSubscriptionsSettings = visibleExpandedSections.has('subscriptions-settings');
|
||||
const showBoardLinkPermissionsSettings = visibleExpandedSections.has('board-link-permissions-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;
|
||||
|
||||
@@ -98,7 +104,7 @@ const SettingsModal = () => {
|
||||
setExpandedSections(new Set());
|
||||
navigate(basePath, { replace: true });
|
||||
} else {
|
||||
setExpandedSections(new Set(allSectionIds));
|
||||
setExpandedSections(new Set(sectionIds));
|
||||
navigate(basePath, { replace: true });
|
||||
}
|
||||
};
|
||||
@@ -185,6 +191,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.7"
|
||||
"@bitsocial/bitsocial-react-hooks": "npm:0.1.8"
|
||||
"@capacitor/android": "npm:7.4.5"
|
||||
"@capacitor/app": "npm:7.0.1"
|
||||
"@capacitor/browser": "npm:7.0.5"
|
||||
@@ -24,7 +24,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"
|
||||
@@ -1523,12 +1523,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bitsocial/bitsocial-react-hooks@npm:0.1.7":
|
||||
version: 0.1.7
|
||||
resolution: "@bitsocial/bitsocial-react-hooks@npm:0.1.7"
|
||||
"@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.8"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.27"
|
||||
"@pkcprotocol/pkc-js": "npm:0.0.29"
|
||||
"@pkcprotocol/pkc-logger": "npm:0.1.0"
|
||||
assert: "npm:2.0.0"
|
||||
ethers: "npm:5.8.0"
|
||||
@@ -1544,7 +1544,7 @@ __metadata:
|
||||
zustand: "npm:4.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8"
|
||||
checksum: 10c0/39bd3f263fac36108127ab11f9161092c50860373bd77fa8d933de75f79fd758215d16ddcad207c319ea01cc0564fd5526857a5517c212e46325977e687e07c1
|
||||
checksum: 10c0/0222c68180ff30372b1ff415bc4c47c47bf1de2ef74bd08457730922498d0eff6ce42b379f96ebefd32a18f37cddff5e2431a7d338634efa7fb6e404e1fc9c2a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3993,28 +3993,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"
|
||||
@@ -4630,7 +4608,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:
|
||||
@@ -4649,6 +4627,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"
|
||||
@@ -5768,69 +5765,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.27":
|
||||
version: 0.0.27
|
||||
resolution: "@pkcprotocol/pkc-js@npm:0.0.27"
|
||||
"@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"
|
||||
@@ -5843,6 +5780,7 @@ __metadata:
|
||||
"@libp2p/identify": "npm:4.1.3"
|
||||
"@libp2p/interfaces": "npm:3.3.2"
|
||||
"@libp2p/peer-id": "npm:6.0.8"
|
||||
"@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"
|
||||
@@ -5879,7 +5817,7 @@ __metadata:
|
||||
typestub-ipfs-only-hash: "npm:4.0.0"
|
||||
uuid: "npm:13.0.0"
|
||||
zod: "npm:4.3.6"
|
||||
checksum: 10c0/19360d831a9ea3ecb586ca02d1763611224bdcc72f2586dc50ffe1a550a56e72c79f173ebce8422ab15623f277889dd96666016ab042d208fb4d185879b82784
|
||||
checksum: 10c0/089100e75d4b2c735f3a9b9f6b087970afb9dc606e21be6e5e1e84daf2e3350793f5cfa8f6508a3db7bc518a1173e517f449174185b2a07f5df7d33f73cffdac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7612,7 +7550,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:
|
||||
@@ -7642,38 +7580,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"
|
||||
@@ -9740,13 +9646,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"
|
||||
@@ -9754,6 +9653,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"
|
||||
@@ -11794,13 +11700,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"
|
||||
@@ -12934,7 +12833,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:
|
||||
@@ -13053,7 +12952,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:
|
||||
@@ -17791,13 +17690,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"
|
||||
@@ -18289,13 +18181,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