diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index 6f7c301f..2b358f92 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index a11629f9..bb98f661 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 0e5d010d..79d9f10c 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/da/default.json b/public/translations/da/default.json index fc434d86..68bd5cba 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 38e60972..fc05a9d3 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/el/default.json b/public/translations/el/default.json index 1a903d50..9be53c93 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/en/default.json b/public/translations/en/default.json index e9d3b50d..bd7240ba 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/es/default.json b/public/translations/es/default.json index fa238682..3a6999bf 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index 859f9349..907d015b 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 3243cb66..d978bb0a 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 9414997a..369fb3b1 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 6dc2a8d9..eaee59d6 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 9931e3bd..19270b2b 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index 1cea3cff..f9f0bbe1 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index 5e87f229..eff4fe45 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/id/default.json b/public/translations/id/default.json index 1203adb9..aaa62104 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/it/default.json b/public/translations/it/default.json index bc84e6a2..c00d51a3 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index da142827..5e472ce7 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index 59171a47..f28f4bf3 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index 9fecc253..59e43915 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index 99b0ff9e..81142018 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/no/default.json b/public/translations/no/default.json index 039ce360..21af0ac1 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index ad2c82c6..9ac705b0 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 60968d84..833ceb97 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 294d962c..bdf6817d 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index f81a0667..260a4e4c 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index b52191a4..475c6491 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index e8b5fb51..63f92588 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/te/default.json b/public/translations/te/default.json index b32a9785..a87d9bed 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/th/default.json b/public/translations/th/default.json index 728dafb7..9a09f646 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index f7ea934c..1ea18167 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index 21ba5bf8..04e2fbc7 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 6efe91f4..539c7f0f 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index c3d3a773..92e42ee9 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 873674ae..984074fa 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -374,7 +374,7 @@ "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.", + "enable_pure_p2p_tip": "Use a browser P2P node instead of centralized IPFS gateways.", "p2p_stats_loading": "Loading P2P stats...", "p2p_stats_updated": "Updated", "p2p_stats_starting": "P2P stats appear after the P2P client starts.", diff --git a/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx b/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx index 146e09c4..21a36566 100644 --- a/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx +++ b/src/components/settings-modal/p2p-stats-settings/__tests__/p2p-stats-settings.test.tsx @@ -42,6 +42,9 @@ const renderSettings = async (isElectron = false) => { }); }; +const getStatRows = () => + new Map(Array.from(container.querySelectorAll('tr')).map((row) => [row.children.item(0)?.textContent ?? '', row.children.item(1)?.textContent ?? ''])); + describe('P2PStatsSettings', () => { beforeEach(() => { vi.clearAllMocks(); @@ -86,7 +89,14 @@ describe('P2PStatsSettings', () => { }, _helia: { libp2p: { - getConnections: () => ['connection-1'], + getConnections: () => [ + { + direction: 'outbound', + remoteAddr: { toString: () => '/ip4/127.0.0.1/tcp/4001/ws/p2p/peer-1' }, + remotePeer: { toString: () => 'peer-1' }, + status: 'open', + }, + ], getMultiaddrs: () => ['/ip4/127.0.0.1/tcp/4001'], getPeers: () => ['peer-1', 'peer-2'], metrics: { @@ -121,16 +131,26 @@ describe('P2PStatsSettings', () => { await renderSettings(false); await act(async () => Promise.resolve()); - expect(container.textContent).toContain('leeching'); + const rows = getStatRows(); + 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(rows.get('Connected peers')).toContain('2 peers, 1 connection'); + expect(rows.get('Connected peers')).toContain('WebSocket'); + expect(rows.get('Connected peers')).toContain('/ip4/127.0.0.1/tcp/4001/ws/p2p/peer-1'); + expect(rows.has('connections')).toBe(false); + expect(rows.has('Listen addresses')).toBe(false); + expect(rows.has('p2p_stats_updated')).toBe(true); expect(container.textContent).toContain('self-peer'); - expect(container.textContent).toContain('2 peers'); - expect(container.textContent).toContain('downloaded data'); + expect(container.textContent).toContain('Peer ID'); + expect(container.textContent).toContain('Data received'); expect(container.textContent).toContain('2.00 KB'); - expect(container.textContent).toContain('uploaded data'); + expect(container.textContent).toContain('Data sent'); expect(container.textContent).toContain('1.00 KB'); + expect(container.textContent).not.toContain('client key'); + expect(container.textContent).not.toContain('pubsub peers'); + expect(container.textContent).not.toContain('routers'); expect(container.textContent).not.toContain('pubsub topics'); expect(container.textContent).not.toContain('topic subscribers'); }); @@ -176,12 +196,45 @@ describe('P2PStatsSettings', () => { await renderSettings(false); await act(async () => Promise.resolve()); - expect(container.textContent).toContain('downloaded data'); + expect(container.textContent).toContain('Data received'); expect(container.textContent).toContain('4.00 KB'); - expect(container.textContent).toContain('uploaded data'); + expect(container.textContent).toContain('Data sent'); expect(container.textContent).toContain('2.00 KB'); }); + it('starts browser transfer counters at zero when Helia exposes no byte totals yet', async () => { + testState.account = { + ...testState.account, + pkcOptions: { + libp2pJsClientsOptions: [{ key: 'libp2pjs' }], + }, + pkc: { + clients: { + libp2pJsClients: { + libp2pjs: { + key: 'libp2pjs', + _helia: { + libp2p: { + getConnections: () => [], + getMultiaddrs: () => [], + getPeers: () => [], + peerId: { toString: () => 'self-peer' }, + }, + }, + }, + }, + }, + }, + }; + + await renderSettings(false); + await act(async () => Promise.resolve()); + + const rows = getStatRows(); + expect(rows.get('Data received')).toBe('0 B'); + expect(rows.get('Data sent')).toBe('0 B'); + }); + it('reports seeding only when browser Helia can add and publish provider records', async () => { testState.account = { ...testState.account, @@ -220,7 +273,7 @@ describe('P2PStatsSettings', () => { await renderSettings(false); await act(async () => Promise.resolve()); - expect(container.textContent).toContain('seeding'); + expect(container.textContent).toContain('Seeding'); expect(container.textContent).not.toContain('seed mode'); }); }); diff --git a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css index 48bfc9c1..21847e5b 100644 --- a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css +++ b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.module.css @@ -31,7 +31,6 @@ .statName { width: 34%; - text-transform: capitalize; white-space: nowrap; } @@ -39,6 +38,46 @@ word-break: break-word; } +.connectedPeers summary { + cursor: pointer; +} + +.connectedPeerList { + display: grid; + gap: 6px; + margin-top: 5px; +} + +.connectedPeer { + border-top: 1px solid var(--border-color, #d9d9d9); + padding-top: 5px; +} + +.connectedPeerMeta { + display: flex; + flex-wrap: wrap; + gap: 5px; + font-size: 0.9em; +} + +.connectionTransport { + font-weight: 700; +} + +.peerId, +.connectionAddress { + word-break: break-all; +} + +.connectionAddress { + display: block; + margin-top: 2px; +} + +.connectedPeerEmpty { + font-style: italic; +} + .statsMeta { margin-top: 5px; font-size: 0.85em; diff --git a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx index 72f4fde4..c854bb86 100644 --- a/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx +++ b/src/components/settings-modal/p2p-stats-settings/p2p-stats-settings.tsx @@ -6,11 +6,31 @@ import styles from './p2p-stats-settings.module.css'; type AccountShape = Record; -type StatRow = { +type TextStatRow = { name: string; + type?: 'text'; value: string; }; +type ConnectedPeerEntry = { + address: string; + direction?: string; + id: string; + peerId: string; + status?: string; + transport: string; +}; + +type ConnectedPeersStatRow = { + connectionCount: number; + entries: ConnectedPeerEntry[]; + name: string; + peerCount: number; + type: 'connectedPeers'; +}; + +type StatRow = ConnectedPeersStatRow | TextStatRow; + type StatsState = { error?: string; loading: boolean; @@ -63,10 +83,18 @@ type TransferStats = { uploadedBytes?: number; }; +type ObservedTransferStats = { + connections: WeakSet; + downloadedBytes: number; + streams: WeakSet; + 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 observedBrowserTransferStats = new WeakMap(); const statsReducer = (state: StatsState, action: StatsAction): StatsState => { if (action.type === 'loading') return { ...state, error: undefined, loading: true }; @@ -94,17 +122,20 @@ const formatBytes = (value: unknown) => { 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 = (value?: Record) => (value ? Object.values(value)[0] : undefined); const isRecord = (value: unknown): value is Record => !!value && typeof value === 'object'; +const getStringValue = (value: unknown, fallback = 'unknown') => { + if (value === null || value === undefined) return fallback; + try { + const stringValue = String(value); + return stringValue || fallback; + } catch { + return fallback; + } +}; + const getFiniteNumber = (value: unknown) => { if (value === null || value === undefined) return undefined; const numericValue = typeof value === 'bigint' ? Number(value) : Number(value); @@ -142,6 +173,24 @@ const getSafeArray = async (getValue?: () => unknown[] | Promise | un } }; +const getByteLength = (value: unknown): number | undefined => { + if (value === null || value === undefined) return undefined; + if (typeof value === 'string') return new TextEncoder().encode(value).byteLength; + if (value instanceof ArrayBuffer) return value.byteLength; + if (ArrayBuffer.isView(value)) return value.byteLength; + if (isRecord(value)) { + const directByteLength = getFiniteNumber(value.byteLength); + if (directByteLength !== undefined) return directByteLength; + const dataByteLength = getByteLength(value.data); + if (dataByteLength !== undefined) return dataByteLength; + } + if (Array.isArray(value)) { + const total = value.reduce((sum, entry) => sum + (getByteLength(entry) ?? 0), 0); + return total > 0 ? total : undefined; + } + return undefined; +}; + const getTransferStatsFromHeliaCounters = (helia: unknown): TransferStats => { const stats: TransferStats = {}; const visited = new WeakSet(); @@ -199,17 +248,18 @@ const classifyTransferMetricPath = (path: string[]) => { 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 snapshots = await Promise.all( + ['getMetrics', 'getMetricValues', 'toJSON'].map(async (method) => { + const candidate = source[method]; + if (typeof candidate !== 'function') return undefined; + try { + return await candidate.call(source); + } catch { + return undefined; + } + }), + ); + return snapshots.find((snapshot) => snapshot !== undefined) ?? source; }; const getTransferStatsFromMetricSnapshot = (snapshot: unknown): TransferStats => { @@ -249,9 +299,92 @@ const mergeTransferStats = (primary: TransferStats, fallback: TransferStats): Tr uploadedBytes: primary.uploadedBytes ?? fallback.uploadedBytes, }); -const getBrowserTransferStats = async (client?: Libp2pClientShape): Promise => { +const getTransferStatsFromClientShape = (client?: Libp2pClientShape): TransferStats => { + const clientRecord = isRecord(client) ? (client as Record) : undefined; + const statsSources = [clientRecord, clientRecord?.stats, clientRecord?.sessionStats].filter(Boolean); + return statsSources.reduce((stats, source) => { + if (!isRecord(source)) return stats; + const downloadedBytes = source.totalIn ?? source.downloadedBytes ?? source.bytesReceived ?? source.receivedBytes; + const uploadedBytes = source.totalOut ?? source.uploadedBytes ?? source.bytesSent ?? source.sentBytes; + addTransferStats(stats, 'downloadedBytes', downloadedBytes); + addTransferStats(stats, 'uploadedBytes', uploadedBytes); + return stats; + }, {}); +}; + +const getObservedTransferStats = (client?: Libp2pClientShape): ObservedTransferStats | undefined => { + if (!isRecord(client)) return undefined; + let stats = observedBrowserTransferStats.get(client); + if (!stats) { + stats = { + connections: new WeakSet(), + downloadedBytes: 0, + streams: new WeakSet(), + uploadedBytes: 0, + }; + observedBrowserTransferStats.set(client, stats); + } + return stats; +}; + +const instrumentStreamTransferStats = (stream: unknown, stats: ObservedTransferStats) => { + if (!isRecord(stream) || stats.streams.has(stream)) return; + stats.streams.add(stream); + + const send = stream.send; + if (typeof send === 'function') { + try { + stream.send = function sendWithTransferStats(this: unknown, data: unknown, ...args: unknown[]) { + addTransferStats(stats, 'uploadedBytes', getByteLength(data)); + return send.call(this, data, ...args); + }; + } catch { + // Some stream implementations may expose read-only methods. + } + } + + const addEventListener = stream.addEventListener; + if (typeof addEventListener === 'function') { + try { + addEventListener.call(stream, 'message', (event: unknown) => { + const data = isRecord(event) ? (event.data ?? event.detail) : undefined; + addTransferStats(stats, 'downloadedBytes', getByteLength(data)); + }); + } catch { + return; + } + } +}; + +const instrumentConnectionTransferStats = (connection: unknown, stats: ObservedTransferStats) => { + if (!isRecord(connection)) return; + + if (!stats.connections.has(connection)) { + stats.connections.add(connection); + const newStream = connection.newStream; + if (typeof newStream === 'function') { + try { + connection.newStream = async function newStreamWithTransferStats(this: unknown, ...args: unknown[]) { + const stream = await newStream.apply(this, args); + instrumentStreamTransferStats(stream, stats); + return stream; + }; + } catch { + // Some connection implementations may expose read-only methods. + } + } + } + + for (const stream of toArray(connection.streams)) instrumentStreamTransferStats(stream, stats); +}; + +const getBrowserTransferStats = async (client?: Libp2pClientShape, connections: unknown[] = []): Promise => { try { const helia = client?._helia; + const observedStats = getObservedTransferStats(client); + if (observedStats) connections.forEach((connection) => instrumentConnectionTransferStats(connection, observedStats)); + + const clientStats = getTransferStatsFromClientShape(client); const counterStats = getTransferStatsFromHeliaCounters(helia); const metricSources = [helia?.metrics, helia?.libp2p?.metrics].filter(Boolean); const metricSnapshots = await Promise.all(metricSources.map((source) => getMetricSnapshot(source))); @@ -259,7 +392,7 @@ const getBrowserTransferStats = async (client?: Libp2pClientShape): Promise getTransferStatsFromMetricSnapshot(snapshot)) .reduce((stats, nextStats) => mergeTransferStats(stats, nextStats), {}); - return mergeTransferStats(counterStats, metricStats); + return mergeTransferStats(mergeTransferStats(mergeTransferStats(clientStats, counterStats), metricStats), observedStats ?? {}); } catch { return {}; } @@ -290,33 +423,98 @@ 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'; + if (!client) return 'Unknown'; + return hasSupportedAdd(client) && hasProviderPublishingRouter(client) ? 'Seeding' : 'Leeching'; +}; + +const getTransportLabel = (address: string) => { + const normalizedAddress = address.toLowerCase(); + let transport = 'Unknown transport'; + if (normalizedAddress.includes('/webtransport')) transport = 'WebTransport'; + else if (normalizedAddress.includes('/webrtc-direct')) transport = 'WebRTC direct'; + else if (normalizedAddress.includes('/webrtc')) transport = 'WebRTC'; + else if (normalizedAddress.includes('/wss')) transport = 'Secure WebSocket'; + else if (normalizedAddress.includes('/ws')) transport = 'WebSocket'; + else if (normalizedAddress.includes('/quic')) transport = 'QUIC'; + else if (normalizedAddress.includes('/tcp')) transport = 'TCP'; + else if (normalizedAddress.includes('/udp')) transport = 'UDP'; + + return normalizedAddress.includes('/p2p-circuit') ? `${transport} through relay` : transport; +}; + +const getConnectionPeerId = (connection: unknown) => (isRecord(connection) ? getStringValue(connection.remotePeer) : 'unknown'); + +const getConnectionAddress = (connection: unknown) => (isRecord(connection) ? getStringValue(connection.remoteAddr, 'address unavailable') : 'address unavailable'); + +const getBrowserConnectedPeersRow = (peers: unknown[], connections: unknown[]): ConnectedPeersStatRow => { + const entries = connections.map((connection) => { + const address = getConnectionAddress(connection); + const peerId = getConnectionPeerId(connection); + const fallbackId = `${peerId}-${address}`; + return { + address, + direction: isRecord(connection) ? getStringValue(connection.direction, '') : undefined, + id: isRecord(connection) ? getStringValue(connection.id, fallbackId) : fallbackId, + peerId, + status: isRecord(connection) ? getStringValue(connection.status, '') : undefined, + transport: getTransportLabel(address), + }; + }); + const peerIds = [...peers, ...entries].reduce>((ids, peerOrEntry) => { + const peerId = isRecord(peerOrEntry) && 'peerId' in peerOrEntry ? getStringValue(peerOrEntry.peerId) : getStringValue(peerOrEntry); + if (peerId && peerId !== 'unknown') ids.add(peerId); + return ids; + }, new Set()); + + return { + connectionCount: connections.length, + entries, + name: 'Connected peers', + peerCount: peerIds.size || entries.length, + type: 'connectedPeers', + }; +}; + +const getElectronConnectedPeersRow = (peers: unknown): ConnectedPeersStatRow => { + const peerEntries = isRecord(peers) && Array.isArray(peers.Peers) ? peers.Peers : []; + const entries = peerEntries.map((peer) => { + const address = isRecord(peer) ? getStringValue(peer.Addr, 'address unavailable') : 'address unavailable'; + const peerId = isRecord(peer) ? getStringValue(peer.Peer) : 'unknown'; + const direction = isRecord(peer) ? getStringValue(peer.Direction, '') : undefined; + return { + address, + direction, + id: `${peerId}-${address}-${direction ?? ''}`, + peerId, + transport: getTransportLabel(address), + }; + }); + const peerIds = entries.reduce>((ids, entry) => { + if (entry.peerId && entry.peerId !== 'unknown') ids.add(entry.peerId); + return ids; + }, new Set()); + + return { + connectionCount: entries.length, + entries, + name: 'Connected peers', + peerCount: peerIds.size || entries.length, + type: 'connectedPeers', + }; }; const getBrowserLibp2pStats = async (account?: AccountShape): Promise => { 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), - ]); + const [peers, connections] = await Promise.all([getSafeArray(() => libp2p?.getPeers?.()), getSafeArray(() => libp2p?.getConnections?.())]); + const transferStats = await getBrowserTransferStats(client, connections); 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) }, + { name: 'Mode', value: getBrowserMode(client) }, + { name: 'Peer ID', value: libp2p?.peerId?.toString() ?? 'unknown' }, + getBrowserConnectedPeersRow(peers, connections), + { name: 'Data received', value: transferStats.downloadedBytes === undefined ? 'unknown' : formatBytes(transferStats.downloadedBytes) }, + { name: 'Data sent', value: transferStats.uploadedBytes === undefined ? 'unknown' : formatBytes(transferStats.uploadedBytes) }, ]; }; @@ -345,17 +543,17 @@ const getElectronKuboStats = async (rpcState?: string, signal?: AbortSignal): Pr ]); return [ - { name: 'mode', value: 'desktop Kubo' }, + { 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') }, + { name: 'Peer ID', value: identity.ID ?? 'unknown' }, + { name: 'Agent', value: identity.AgentVersion ?? version.Version ?? 'unknown' }, + getElectronConnectedPeersRow(peers), + { 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') }, ]; }; @@ -364,6 +562,33 @@ const getP2PStats = async (mode: P2PRuntimeMode, account?: AccountShape, rpcStat return getElectronKuboStats(rpcState, signal); }; +const ConnectedPeersValue = ({ row }: { row: ConnectedPeersStatRow }) => ( +
+ + {formatCount(row.peerCount, 'peer')}, {formatCount(row.connectionCount, 'connection')} + +
+ {row.entries.length ? ( + row.entries.map((entry) => ( +
+
+ {entry.transport} + {entry.direction && {entry.direction}} + {entry.status && {entry.status}} +
+
{entry.peerId}
+ {entry.address} +
+ )) + ) : ( +
No active peer addresses
+ )} +
+
+); + +const StatValue = ({ row }: { row: StatRow }) => (row.type === 'connectedPeers' ? : row.value); + const P2PStatsSettings = () => { const { t } = useTranslation(); const account = useAccount() as AccountShape | undefined; @@ -413,13 +638,21 @@ const P2PStatsSettings = () => { {statsState.rows.map((row) => ( {row.name} - {row.value} + + + ))} + {updatedAtLabel && ( + + {t('p2p_stats_updated')} + {updatedAtLabel} + + )}
- {statsState.loading ? t('p2p_stats_loading') : updatedAtLabel ? `${t('p2p_stats_updated')} ${updatedAtLabel}` : null} + {statsState.loading ? t('p2p_stats_loading') : null} {statsState.error &&
{statsState.error}
}