@@ -4792,6 +4952,8 @@ function App() {
const [menuOpen, setMenuOpen] = useState(false);
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
+ const [notifDisabled, setNotifDisabled] = useState(false);
+ const [userPrefs, setUserPrefs] = useState({});
const resolved = resolveTheme(theme);
@@ -4828,20 +4990,28 @@ function App() {
}, [theme, resolved]);
const fetchNotifications = useCallback(() => {
- if (!user) return;
+ if (!user || notifDisabled) {
+ setNotifications([]); setUnreadCount(0); return;
+ }
hubFetch('/v1/notifications?limit=20', { token: user.token })
.then(data => {
setNotifications(data.notifications || []);
setUnreadCount(data.unread_count || 0);
})
.catch(() => {});
- }, [user]);
+ }, [user, notifDisabled]);
useEffect(() => {
if (!user) { setGroups([]); setNotifications([]); setUnreadCount(0); return; }
hubFetch('/v1/groups/mine', { token: user.token })
.then(data => setGroups(data.groups || []))
.catch(() => setGroups([]));
+ hubFetch('/v1/users/me/preferences', { token: user.token })
+ .then(prefs => {
+ setUserPrefs(prefs || {});
+ if (prefs.notifications_disabled === 'true') setNotifDisabled(true);
+ })
+ .catch(() => {});
fetchNotifications();
}, [user]);
@@ -5062,6 +5232,7 @@ function App() {
page = html`<${GroupPage}
groupId=${groupId} group=${group} token=${user.token}
username=${user.username} userId=${user.userId}
+ userPrefs=${userPrefs}
onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications}
onGroupUpdated=${updateGroup} onPresence=${notePresence}
onLeft=${handleLeftGroup} />`;
@@ -5072,7 +5243,15 @@ function App() {
onMarkRead=${markRead} onPurge=${purgeNotifications} />`;
} else if (route === '/settings') {
page = html`<${SettingsPage} user=${user} theme=${theme}
- onThemeChange=${setTheme} groups=${groups} />`;
+ onThemeChange=${setTheme} groups=${groups}
+ onPrefsChange=${(p) => {
+ if ('notifications_disabled' in p) {
+ setNotifDisabled(p.notifications_disabled);
+ if (p.notifications_disabled) { setNotifications([]); setUnreadCount(0); }
+ else fetchNotifications();
+ }
+ setUserPrefs(prev => ({ ...prev, ...p }));
+ }} />`;
} else if (route === '/profile') {
page = html`<${ProfilePage} user=${user} onLogout=${authCtx.logout} />`;
} else {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index df72382..ec56f03 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -127,6 +127,7 @@ export default {
'status.idle': 'Inaktiv',
'status.discovering': 'Nodes werden gesucht …',
'status.connecting': 'Verbindung über WebRTC …',
+ 'status.connecting_short': 'Verbindung wird hergestellt…',
'status.fetching': 'Index wird abgerufen …',
'status.files': {
one: '{n} Datei',
@@ -169,15 +170,17 @@ export default {
'settings.dl_choose': 'Ordner auswählen',
'settings.dl_change': 'Ändern',
'settings.dl_forget': 'Verwerfen',
- 'settings.dl_path_note': 'Einer Webseite lässt sich kein Pfad übergeben, hier ist '
- + 'also nichts einzutippen: Ihr Browser gewährt Zugriff auf den Ordner, den Sie '
- + 'auswählen, und MeshBay schreibt ausschließlich darin. Einmal pro Sitzung kann '
- + 'eine Bestätigung verlangt werden.',
- 'settings.dl_unsupported': 'Dieser Browser kann nicht in einen Ordner Ihrer Wahl '
- + 'schreiben (keine File System Access API), daher landen Downloads in seinem '
- + 'eigenen Download-Ordner. Chrome und Edge lassen die Wahl zu.',
+
'settings.profile': 'Profil',
'settings.username': 'Benutzername',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Standardwerte',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Node-Identitäten',
'settings.node_pins_hint': 'Der Identitätsschlüssel jedes Nodes wird bei der ersten '
+ 'Verbindung gemerkt. Ändert er sich, wird die Verbindung abgelehnt — das ist nur '
@@ -452,6 +455,8 @@ export default {
'chat.jump_new': 'Neue Nachrichten',
'group.leave': 'Gruppe verlassen',
'group.leave_confirm': '„{name}“ verlassen? Sie verlieren den Zugang zu ihren Dateien und zum Chat. Hochgeladene Dateien bleiben auf dem Node, und der Node behält die für Sie gemerkte Identität, bis sein Betreiber sie entfernt.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Profil',
'usermenu.profile': 'Profil',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index ce14f0b..cb4b349 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -126,6 +126,7 @@ export default {
'status.idle': 'Idle',
'status.discovering': 'Finding nodes...',
'status.connecting': 'Connecting via WebRTC...',
+ 'status.connecting_short': 'Connecting...',
'status.fetching': 'Fetching index...',
'status.files': {
one: '{n} file',
@@ -167,14 +168,16 @@ export default {
'settings.dl_choose': 'Choose folder',
'settings.dl_change': 'Change',
'settings.dl_forget': 'Forget',
- 'settings.dl_path_note': 'A web page cannot be given a path, so there is nothing '
- + 'to type here: your browser grants access to the folder you pick, and MeshBay '
- + 'only ever writes inside it. You may be asked to confirm once per session.',
- 'settings.dl_unsupported': 'This browser cannot write into a folder of your '
- + 'choosing (no File System Access API), so downloads go to its own download '
- + 'folder. Chrome and Edge support choosing one.',
'settings.profile': 'Profile',
'settings.username': 'Username',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Defaults',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Node identities',
'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.",
'settings.node_pins_count': {
@@ -437,6 +440,8 @@ export default {
'chat.jump_new': 'New messages',
'group.leave': 'Leave group',
'group.leave_confirm': 'Leave “{name}”? You will lose access to its files and chat. Files you uploaded stay on the node, and the node keeps the identity it pinned for you until its operator removes it.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Profile',
'usermenu.profile': 'Profile',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 2d9084b..5afb3c3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -125,6 +125,7 @@ export default {
'status.idle': 'Inactivo',
'status.discovering': 'Buscando nodes...',
'status.connecting': 'Conectando por WebRTC...',
+ 'status.connecting_short': 'Conectando…',
'status.fetching': 'Obteniendo el índice...',
'status.files': {
one: '{n} archivo',
@@ -167,15 +168,17 @@ export default {
'settings.dl_choose': 'Elegir carpeta',
'settings.dl_change': 'Cambiar',
'settings.dl_forget': 'Olvidar',
- 'settings.dl_path_note': 'A una página web no se le puede indicar una ruta, así que '
- + 'aquí no hay nada que escribir: su navegador concede acceso a la carpeta que '
- + 'usted señale, y MeshBay solo escribe dentro de ella. Es posible que se le pida '
- + 'confirmación una vez por sesión.',
- 'settings.dl_unsupported': 'Este navegador no puede escribir en una carpeta de su '
- + 'elección (no tiene File System Access API), de modo que las descargas van a su '
- + 'propia carpeta de descargas. Chrome y Edge sí permiten elegir una.',
+
'settings.profile': 'Perfil',
'settings.username': 'Nombre de usuario',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Valores predeterminados',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Identidades de los nodes',
'settings.node_pins_hint': 'La clave de identidad de cada node se memoriza la '
+ 'primera vez que se conecta. Si cambia, la conexión se rechaza — algo esperable '
@@ -447,6 +450,8 @@ export default {
'chat.jump_new': 'Mensajes nuevos',
'group.leave': 'Salir del grupo',
'group.leave_confirm': '¿Salir de «{name}»? Perderá el acceso a sus archivos y a su chat. Los archivos que subió permanecen en el node, y este conserva la identidad que fijó para usted hasta que su operador la retire.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Perfil',
'usermenu.profile': 'Perfil',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 9aa2857..fe6c666 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -126,6 +126,7 @@ export default {
'status.idle': 'Inactif',
'status.discovering': 'Recherche de nodes...',
'status.connecting': 'Connexion via WebRTC...',
+ 'status.connecting_short': 'Connexion…',
'status.fetching': "Récupération de l'index...",
'status.files': {
one: '{n} fichier',
@@ -168,16 +169,17 @@ export default {
'settings.dl_choose': 'Choisir un dossier',
'settings.dl_change': 'Changer',
'settings.dl_forget': 'Oublier',
- 'settings.dl_path_note': 'On ne peut pas indiquer un chemin à une page web, il n’y '
- + 'a donc rien à saisir ici : votre navigateur donne accès au dossier que vous '
- + 'désignez, et MeshBay n’écrit jamais ailleurs qu’à l’intérieur. Une '
- + 'confirmation peut vous être demandée une fois par session.',
- 'settings.dl_unsupported': 'Ce navigateur ne sait pas écrire dans un dossier de '
- + 'votre choix (pas de File System Access API) ; les téléchargements vont donc '
- + 'dans son propre dossier de téléchargement. Chrome et Edge permettent d’en '
- + 'choisir un.',
+
'settings.profile': 'Profil',
'settings.username': "Nom d'utilisateur",
+ 'settings.email': 'E-mail',
+ 'settings.email_save': 'Enregistrer',
+ 'settings.email_saved': 'E-mail mis à jour',
+ 'settings.notif_global_disable': 'Désactiver toutes les notifications',
+ 'settings.notif_global_hint': 'Quand activé, aucune notification n\x27est créée pour aucun groupe.',
+ 'settings.defaults': 'Valeurs par défaut',
+ 'settings.default_tab': 'Onglet par défaut',
+ 'settings.default_tab_hint': 'L\'onglet qui s\'ouvre en premier quand vous entrez dans un groupe.',
'settings.node_pins': 'Identités des nodes',
'settings.node_pins_hint': "La clé d'identité de chaque node est mémorisée lors de "
+ 'la première connexion. Si elle change, la connexion est refusée — ce qui n’est '
@@ -452,6 +454,8 @@ export default {
'chat.jump_new': 'Nouveaux messages',
'group.leave': 'Quitter le groupe',
'group.leave_confirm': 'Quitter « {name} » ? Vous perdrez l’accès à ses fichiers et à sa discussion. Les fichiers que vous avez envoyés restent sur le node, et celui-ci conserve l’identité qu’il a épinglée pour vous jusqu’à ce que son opérateur la retire.',
+ 'group.mute': 'Couper les notifications',
+ 'group.unmute': 'Réactiver les notifications',
'profile.title': 'Profil',
'usermenu.profile': 'Profil',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 379e7cb..5653ec0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -126,6 +126,7 @@ export default {
'status.idle': 'Inattivo',
'status.discovering': 'Ricerca dei node...',
'status.connecting': 'Connessione tramite WebRTC...',
+ 'status.connecting_short': 'Connessione…',
'status.fetching': "Recupero dell'indice...",
'status.files': {
one: '{n} file',
@@ -168,15 +169,17 @@ export default {
'settings.dl_choose': 'Scegli una cartella',
'settings.dl_change': 'Cambia',
'settings.dl_forget': 'Dimentica',
- 'settings.dl_path_note': 'A una pagina web non si può indicare un percorso, quindi '
- + 'qui non c’è nulla da digitare: è il browser a concedere l’accesso alla cartella '
- + 'che indica, e MeshBay scrive soltanto al suo interno. Potrebbe esserle chiesta '
- + 'una conferma una volta per sessione.',
- 'settings.dl_unsupported': 'Questo browser non sa scrivere in una cartella a sua '
- + 'scelta (manca la File System Access API), perciò i download finiscono nella sua '
- + 'cartella di download. Chrome ed Edge permettono di sceglierne una.',
+
'settings.profile': 'Profilo',
'settings.username': 'Nome utente',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Valori predefiniti',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Identità dei node',
'settings.node_pins_hint': "La chiave d'identità di ogni node viene memorizzata alla "
+ 'prima connessione. Se cambia, la connessione viene rifiutata — cosa che ci si '
@@ -449,6 +452,8 @@ export default {
'chat.jump_new': 'Nuovi messaggi',
'group.leave': 'Esci dal gruppo',
'group.leave_confirm': 'Uscire da «{name}»? Perderà l’accesso ai suoi file e alla chat. I file che ha caricato restano sul node, e il node mantiene l’identità che ha fissato per lei finché il suo operatore non la rimuove.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Profilo',
'usermenu.profile': 'Profilo',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 45e7aa5..aa3899d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -124,6 +124,7 @@ export default {
'status.idle': '待機中',
'status.discovering': 'node を探しています…',
'status.connecting': 'WebRTC で接続しています…',
+ 'status.connecting_short': '接続中…',
'status.fetching': 'インデックスを取得しています…',
'status.files': {
other: '{n} 個のファイル',
@@ -165,16 +166,17 @@ export default {
'settings.dl_choose': 'フォルダーを選択',
'settings.dl_change': '変更',
'settings.dl_forget': '解除',
- 'settings.dl_path_note': 'ウェブページにパスを指定することはできないため、'
- + 'ここに入力するものはありません。お選びになったフォルダーへのアクセスは'
- + 'ブラウザーが許可し、MeshBay はその中にしか書き込みません。'
- + 'セッションごとに 1 回、確認を求められることがあります。',
- 'settings.dl_unsupported': 'このブラウザーはお好きなフォルダーへの書き込みに'
- + '対応していないため(File System Access API がありません)、'
- + 'ダウンロードはブラウザー自身のダウンロードフォルダーに保存されます。'
- + 'Chrome と Edge では選択できます。',
+
'settings.profile': 'プロフィール',
'settings.username': 'ユーザー名',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'デフォルト',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'node の識別情報',
'settings.node_pins_hint': '各 node の識別鍵は、最初に接続したときに記憶されます。'
+ 'それが変わった場合、接続は拒否されます。これが起こるのは、運営者が node を'
@@ -438,6 +440,8 @@ export default {
'chat.jump_new': '新しいメッセージ',
'group.leave': 'グループを退出',
'group.leave_confirm': '「{name}」を退出しますか?ファイルとチャットへのアクセスがなくなります。アップロードしたファイルは node に残り、node は固定した識別情報を運営者が削除するまで保持します。',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'プロフィール',
'usermenu.profile': 'プロフィール',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 37d3e24..c5841dd 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -127,6 +127,7 @@ export default {
'status.idle': 'Inactief',
'status.discovering': 'Nodes zoeken...',
'status.connecting': 'Verbinden via WebRTC...',
+ 'status.connecting_short': 'Verbinden…',
'status.fetching': 'Index ophalen...',
'status.files': {
one: '{n} bestand',
@@ -169,15 +170,17 @@ export default {
'settings.dl_choose': 'Map kiezen',
'settings.dl_change': 'Wijzigen',
'settings.dl_forget': 'Vergeten',
- 'settings.dl_path_note': 'Aan een webpagina kan geen pad worden doorgegeven, dus '
- + 'hier valt niets in te typen: uw browser verleent toegang tot de map die u '
- + 'aanwijst, en MeshBay schrijft uitsluitend daarbinnen. Mogelijk wordt u één keer '
- + 'per sessie om bevestiging gevraagd.',
- 'settings.dl_unsupported': 'Deze browser kan niet schrijven naar een map van uw keuze '
- + '(geen File System Access API), dus downloads gaan naar zijn eigen downloadmap. '
- + 'Chrome en Edge laten wel een keuze toe.',
+
'settings.profile': 'Profiel',
'settings.username': 'Gebruikersnaam',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Standaardwaarden',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Node-identiteiten',
'settings.node_pins_hint': 'De identiteitssleutel van elke node wordt bij de eerste '
+ 'verbinding onthouden. Verandert die, dan wordt de verbinding geweigerd — wat '
@@ -451,6 +454,8 @@ export default {
'chat.jump_new': 'Nieuwe berichten',
'group.leave': 'Groep verlaten',
'group.leave_confirm': '„{name}” verlaten? U verliest de toegang tot de bestanden en de chat ervan. Bestanden die u hebt geüpload blijven op de node, en de node houdt de voor u vastgezette identiteit tot zijn beheerder die weghaalt.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Profiel',
'usermenu.profile': 'Profiel',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 6766515..eec144b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -130,6 +130,7 @@ export default {
'status.idle': 'Bezczynny',
'status.discovering': 'Szukanie nodes...',
'status.connecting': 'Łączenie przez WebRTC...',
+ 'status.connecting_short': 'Łączenie…',
'status.fetching': 'Pobieranie indeksu...',
'status.files': {
one: '{n} plik',
@@ -174,15 +175,17 @@ export default {
'settings.dl_choose': 'Wybierz folder',
'settings.dl_change': 'Zmień',
'settings.dl_forget': 'Zapomnij',
- 'settings.dl_path_note': 'Stronie internetowej nie da się podać ścieżki, więc nie ma '
- + 'tu czego wpisywać: to przeglądarka udziela dostępu do wskazanego folderu, a '
- + 'MeshBay zapisuje wyłącznie w jego wnętrzu. Raz na sesję może pojawić się prośba '
- + 'o potwierdzenie.',
- 'settings.dl_unsupported': 'Ta przeglądarka nie potrafi zapisywać w dowolnie '
- + 'wybranym folderze (brak File System Access API), więc pobrane pliki trafiają do '
- + 'jej własnego folderu pobierania. Chrome i Edge pozwalają wybrać folder.',
+
'settings.profile': 'Profil',
'settings.username': 'Nazwa użytkownika',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Wartości domyślne',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Tożsamości nodes',
'settings.node_pins_hint': 'Klucz tożsamości każdego node jest zapamiętywany przy '
+ 'pierwszym połączeniu. Jeśli się zmieni, połączenie zostanie odrzucone — czego '
@@ -464,6 +467,8 @@ export default {
'chat.jump_new': 'Nowe wiadomości',
'group.leave': 'Opuść grupę',
'group.leave_confirm': 'Opuścić grupę „{name}”? Utraci Pan(i) dostęp do jej plików i czatu. Wysłane pliki pozostaną na node, a node zachowa przypiętą dla Pana/Pani tożsamość do czasu, aż jego operator ją usunie.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Profil',
'usermenu.profile': 'Profil',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index fa46887..ce2adf2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -127,6 +127,7 @@ export default {
'status.idle': 'Ocioso',
'status.discovering': 'Procurando nodes...',
'status.connecting': 'Conectando via WebRTC...',
+ 'status.connecting_short': 'Conectando…',
'status.fetching': 'Obtendo o índice...',
'status.files': {
one: '{n} arquivo',
@@ -169,15 +170,17 @@ export default {
'settings.dl_choose': 'Escolher pasta',
'settings.dl_change': 'Alterar',
'settings.dl_forget': 'Esquecer',
- 'settings.dl_path_note': 'Não é possível informar um caminho a uma página web, '
- + 'portanto não há nada a digitar aqui: o seu navegador concede acesso à pasta que '
- + 'você indicar, e o MeshBay só escreve dentro dela. Talvez seja pedida uma '
- + 'confirmação uma vez por sessão.',
- 'settings.dl_unsupported': 'Este navegador não consegue escrever em uma pasta de sua '
- + 'escolha (não tem a File System Access API), então os downloads vão para a pasta '
- + 'de downloads dele. Chrome e Edge permitem escolher uma.',
+
'settings.profile': 'Perfil',
'settings.username': 'Nome de usuário',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': 'Padrões',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'Identidades dos nodes',
'settings.node_pins_hint': 'A chave de identidade de cada node é memorizada na '
+ 'primeira conexão. Se ela mudar, a conexão é recusada — o que só é esperado '
@@ -448,6 +451,8 @@ export default {
'chat.jump_new': 'Mensagens novas',
'group.leave': 'Sair do grupo',
'group.leave_confirm': 'Sair de “{name}”? Você perderá o acesso aos arquivos e à conversa. Os arquivos que você enviou permanecem no node, e ele mantém a identidade que fixou para você até que o operador dele a remova.',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': 'Perfil',
'usermenu.profile': 'Perfil',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 3276db8..f249835 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -122,6 +122,7 @@ export default {
'status.idle': '空闲',
'status.discovering': '正在查找 node…',
'status.connecting': '正在通过 WebRTC 连接…',
+ 'status.connecting_short': '正在连接…',
'status.fetching': '正在获取索引…',
'status.files': {
other: '{n} 个文件',
@@ -161,13 +162,17 @@ export default {
'settings.dl_choose': '选择文件夹',
'settings.dl_change': '更改',
'settings.dl_forget': '忘记',
- 'settings.dl_path_note': '网页无法被指定一个路径,因此这里没有什么需要输入:'
- + '由您的浏览器授予对所选文件夹的访问权限,而 MeshBay 只会写入该文件夹之内。'
- + '每个会话可能会要求您确认一次。',
- 'settings.dl_unsupported': '此浏览器无法写入您指定的文件夹(不支持 File System '
- + 'Access API),因此下载内容会进入它自己的下载文件夹。Chrome 和 Edge 支持自行选择。',
+
'settings.profile': '个人资料',
'settings.username': '用户名',
+ 'settings.email': 'Email',
+ 'settings.email_save': 'Save',
+ 'settings.email_saved': 'Email updated',
+ 'settings.notif_global_disable': 'Disable all notifications',
+ 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.',
+ 'settings.defaults': '默认值',
+ 'settings.default_tab': 'Default tab',
+ 'settings.default_tab_hint': 'Which tab opens first when you enter a group.',
'settings.node_pins': 'node 身份',
'settings.node_pins_hint': '每个 node 的身份密钥都会在您首次连接时被记住。'
+ '如果它发生变化,连接会被拒绝——只有当运营者重装 node 时才应如此。'
@@ -421,6 +426,8 @@ export default {
'chat.jump_new': '新消息',
'group.leave': '退出群组',
'group.leave_confirm': '退出“{name}”?您将失去其文件和聊天的访问权。您上传的文件仍留在 node 上,node 也会保留它为您固定的身份,直到其运营者将其移除。',
+ 'group.mute': 'Mute notifications',
+ 'group.unmute': 'Unmute notifications',
'profile.title': '个人资料',
'usermenu.profile': '个人资料',
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index 372b666..6f8f654 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -189,6 +189,7 @@ export async function nativeSave(suggestedName, { auto = true } = {}) {
close: () => sink.close(),
abort: () => sink.abort(),
},
+ open: sink.open ? () => sink.open() : null,
};
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index 238f33a..9ff5923 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -442,17 +442,6 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
}
.group-header h2 { margin-bottom: 0; }
-.status-badge {
- display: inline-block;
- padding: 3px 10px;
- border-radius: 12px;
- font-size: 0.75em;
- font-weight: 500;
-}
-.status-ok { background: #16a34a20; color: var(--success); }
-.status-err { background: var(--error-bg); color: var(--error); }
-.status-busy { background: var(--bg-raised); color: var(--text-secondary); }
-
/* ── Group tabs ──────────────────────────────────────────────────────────── */
.group-tabs {
@@ -467,19 +456,35 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
- padding: 8px 20px;
+ padding: 10px 18px;
color: var(--text-secondary);
- font-size: 0.9em;
- font-weight: 500;
cursor: pointer;
border-radius: 0;
transition: color 0.12s, border-color 0.12s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.group-tab:hover { color: var(--text); background: none; }
.group-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
+.tab-icon { width: 22px; height: 22px; }
+
+.group-mute-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-dim);
+ padding: 4px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+}
+.group-mute-btn:hover { color: var(--text); }
+.group-mute-btn .icon { width: 20px; height: 20px; }
/* ── Chat panel ──────────────────────────────────────────────────────────── */
@@ -998,7 +1003,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
color: var(--text);
font-size: 0.9em;
cursor: pointer;
- min-width: 120px;
+ min-width: 180px;
}
.settings-select:focus { outline: none; border-color: var(--border-focus); }
@@ -1054,6 +1059,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
flex-shrink: 0;
}
.video-close:hover { background: rgba(255, 255, 255, 0.25); }
+.video-close.dl-active { background: rgba(34, 197, 94, 0.25); pointer-events: none; }
.video-container {
width: 100%;
@@ -1457,7 +1463,11 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
/* Green while transfers are running: the count in the badge says how many, but
the colour is what is readable without looking at it. Back to the ordinary
nav colour the moment the last one finishes. */
-.transfer-btn.active .icon { color: var(--success); }
+@keyframes pulse-green {
+ 0%, 100% { color: #86efac; }
+ 50% { color: #15803d; }
+}
+.transfer-btn.active .icon { animation: pulse-green 2s ease-in-out infinite; }
.transfer-wrap { position: relative; display: flex; align-items: center; }
.transfer-btn {
@@ -1504,6 +1514,11 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
text-overflow: ellipsis;
white-space: nowrap;
}
+a.transfer-name {
+ color: var(--link);
+ text-decoration: underline;
+ cursor: pointer;
+}
.transfer-cancel {
background: none;
border: none;
diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py
index c0326f0..8372e5c 100644
--- a/packages/meshbay-node/src/meshbay_node/config.py
+++ b/packages/meshbay-node/src/meshbay_node/config.py
@@ -70,6 +70,10 @@ quic_port = 19010
# and its files stay in the index,
# rather than looking deleted
+# upload_dir: a separate directory for uploads. Files land directly in it,
+# not in an "uploads" subdirectory. It appears as its own root in the index.
+# upload_dir = "/home/user/Incoming"
+
# The single-directory form still works and means the same thing — one root,
# named after the directory, receiving uploads.
[[groups]]
@@ -125,6 +129,7 @@ class RootSpec:
name: str = "" # empty → the directory's basename, derived at load
kind: str = "generic" # generic|video|audio|photo — a view hint, unused for now
upload: bool = False # exactly one root per group receives uploads
+ direct: bool = False # uploads land at root path, not in a subdirectory
@dataclass
@@ -137,6 +142,7 @@ class GroupConfig:
# unprefixed shape.
roots: list[RootSpec] = field(default_factory=list)
shared_dir: str = "" # legacy single-root form, migrated at load
+ upload_dir: str = "" # separate filesystem path for uploads
visibility: str = "private" # public|private — discoverability, not admission
# Admission. "invite" (default) means a newcomer needs a one-time pairing code
# before the node wraps the group key for them; "open" means the node pins
@@ -160,6 +166,11 @@ class GroupConfig:
"""
if not self.roots and self.shared_dir.strip():
self.roots = [RootSpec(path=self.shared_dir.strip(), upload=True)]
+ if self.upload_dir.strip():
+ for r in self.roots:
+ r.upload = False
+ self.roots.append(RootSpec(
+ path=self.upload_dir.strip(), upload=True, direct=True))
@dataclass
@@ -275,6 +286,7 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
# Ignored when roots are given explicitly (warned about in
# _read_roots); otherwise __post_init__ migrates it.
shared_dir="" if _read_roots(g) else g.get("shared_dir", ""),
+ upload_dir=g.get("upload_dir", ""),
visibility=g.get("visibility", "private"),
join_policy=g.get("join_policy", "invite"),
quic_port=g.get("quic_port", cfg.node.quic_port),
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 45aca7a..6d4f172 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -799,6 +799,8 @@ def main() -> None:
"denylist clear")
parser.add_argument("--dir", default=None,
help="shared directory, for group add")
+ parser.add_argument("--upload-dir", default=None,
+ help="separate upload directory, for group add")
parser.add_argument("--yes", action="store_true",
help="skip the confirmation for destructive commands")
parser.add_argument("--config", type=Path, default=None,
@@ -1147,7 +1149,9 @@ def main() -> None:
f"{g.get('peers', 0)} peer(s)")
print(f" {g['id']}")
for r in g.get("roots", []):
- flags = " (uploads)" if r.get("upload") else ""
+ flags = ""
+ if r.get("upload"):
+ flags = " (uploads, direct)" if r.get("direct") else " (uploads)"
live = "" if r.get("available", True) else " [UNAVAILABLE]"
print(f" root {r['name']}{flags}{live}")
if not g.get("has_gek"):
@@ -1156,20 +1160,25 @@ def main() -> None:
return
if args.subcommand != "add":
- print("usage: meshbay-node group list|add
--dir ")
+ print("usage: meshbay-node group list|add --dir [--upload-dir ]")
sys.exit(1)
if not args.target or not args.dir:
- print("usage: meshbay-node group add --dir ")
+ print("usage: meshbay-node group add --dir [--upload-dir ]")
print()
print("The group must already exist on the hub and be yours. This")
print("only tells the node to host it, and picks the directory.")
+ print("--upload-dir sets a separate directory for uploaded files.")
sys.exit(1)
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
- out = _daemon_api(cfg, "/api/groups/attach", method="POST",
- body={"name": args.target, "shared_dir": args.dir})
+ body = {"name": args.target, "shared_dir": args.dir}
+ if args.upload_dir:
+ body["upload_dir"] = args.upload_dir
+ out = _daemon_api(cfg, "/api/groups/attach", method="POST", body=body)
print(f"{out['name']} ({out['group_id'][:8]}) added to {out['config']}")
print(f" shared_dir {out['shared_dir']}")
+ if out.get("upload_dir"):
+ print(f" upload_dir {out['upload_dir']}")
print()
print("Tell the daemon to re-read its config, then give the group a key:")
print(" meshbay-node reload")
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index c111581..20345bb 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -315,7 +315,8 @@ async def list_groups(state: dict) -> dict:
return {"groups": out}
-async def attach_group(state: dict, name: str, shared_dir: str) -> dict:
+async def attach_group(state: dict, name: str, shared_dir: str,
+ upload_dir: str = "") -> dict:
"""
Write a new [[groups]] block into node.toml.
@@ -360,19 +361,30 @@ async def attach_group(state: dict, name: str, shared_dir: str) -> dict:
block = (f'\n[[groups]]\n'
f'id = "{group["id"]}"\n'
f'name = "{group["name"]}"\n'
- f'visibility = "{group.get("visibility", "private")}"\n'
- f'\n [[groups.roots]]\n'
- f' path = "{path}"\n'
- f' upload = true\n')
+ f'visibility = "{group.get("visibility", "private")}"\n')
+ if upload_dir:
+ upload_path = Path(upload_dir).expanduser()
+ try:
+ upload_path.mkdir(parents=True, exist_ok=True)
+ except OSError as e:
+ raise OpError(f"Cannot create {upload_path}: {e}") from e
+ block += f'upload_dir = "{upload_path}"\n'
+ block += (f'\n [[groups.roots]]\n'
+ f' path = "{path}"\n')
+ if not upload_dir:
+ block += f' upload = true\n'
try:
with conf_path.open("a") as f:
f.write(block)
except OSError as e:
raise OpError(f"Cannot write {conf_path}: {e}", status=500) from e
- return {"group_id": group["id"], "name": group["name"],
- "shared_dir": str(path), "config": str(conf_path),
- "note": "restart the node to pick it up"}
+ result = {"group_id": group["id"], "name": group["name"],
+ "shared_dir": str(path), "config": str(conf_path),
+ "note": "restart the node to pick it up"}
+ if upload_dir:
+ result["upload_dir"] = str(upload_path)
+ return result
async def add_root(state: dict, group_id: str, path: str, *,
diff --git a/packages/meshbay-node/src/meshbay_node/roots.py b/packages/meshbay-node/src/meshbay_node/roots.py
index 8f999d7..bc27bf7 100644
--- a/packages/meshbay-node/src/meshbay_node/roots.py
+++ b/packages/meshbay-node/src/meshbay_node/roots.py
@@ -51,6 +51,7 @@ class Root:
path: Path
kind: str = "generic"
upload: bool = False
+ direct: bool = False
# Runtime, not configuration: set by the indexer when the directory can no
# longer be read, and cleared when it comes back.
available: bool = True
@@ -139,7 +140,8 @@ class RootSet:
kind = "generic"
root = Root(name=name, path=path, kind=kind,
- upload=bool(spec.get("upload", False)))
+ upload=bool(spec.get("upload", False)),
+ direct=bool(spec.get("direct", False)))
_refuse_nesting(root, roots)
roots.append(root)
by_folded[root.folded] = root
@@ -277,11 +279,14 @@ class RootSet:
def describe(self) -> list[dict]:
"""Per-root state for the index payload and the admin UI."""
- return [
- {"name": r.name, "kind": r.kind, "available": r.available,
- "upload": r.upload}
- for r in self.roots
- ]
+ out = []
+ for r in self.roots:
+ d: dict = {"name": r.name, "kind": r.kind,
+ "available": r.available, "upload": r.upload}
+ if r.direct:
+ d["direct"] = True
+ out.append(d)
+ return out
def entry_abs_path(roots: RootSet, entry) -> Path | None:
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 22e5e15..f182ab2 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -2087,21 +2087,20 @@ class WebRTCPeerSession:
"filename": filename})
return
- # One destination, chosen by the operator and not by the client:
- # uploads/ inside the group's designated root. C5a is still honoured —
- # the name passed the allowlist above, and an existing file is never
- # replaced, which was the real defect (overwriting a file also made the
- # attacker its recorded uploader, and therefore able to delete it).
- rel_dir = f"{upload_root.name}/{UPLOAD_DIR_NAME}"
- target_dir = upload_root.path / UPLOAD_DIR_NAME
- try:
- target_dir.mkdir(parents=True, exist_ok=True)
- except OSError as e:
- log.warning("Cannot create upload folder in root %r: %s",
- upload_root.name, e)
- self._send({"type": "error", "detail": "Upload folder unavailable",
- "filename": filename})
- return
+ if upload_root.direct:
+ rel_dir = upload_root.name
+ target_dir = upload_root.path
+ else:
+ rel_dir = f"{upload_root.name}/{UPLOAD_DIR_NAME}"
+ target_dir = upload_root.path / UPLOAD_DIR_NAME
+ try:
+ target_dir.mkdir(parents=True, exist_ok=True)
+ except OSError as e:
+ log.warning("Cannot create upload folder in root %r: %s",
+ upload_root.name, e)
+ self._send({"type": "error", "detail": "Upload folder unavailable",
+ "filename": filename})
+ return
upload_key = f"{rel_dir}/{filename}"
state = self._uploads.get(upload_key)
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index 050829e..74a7c8a 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -136,6 +136,7 @@ def create_ui_app(state: dict) -> FastAPI:
state,
(payload.get("name") or "").strip(),
(payload.get("shared_dir") or "").strip(),
+ upload_dir=(payload.get("upload_dir") or "").strip(),
))
@app.delete("/api/groups/{group_id}/files/{file_id}")
--
cgit v1.2.3