aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js39
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/search-page.js117
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js26
14 files changed, 182 insertions, 46 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index 0f28adf..988bcb1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -149,6 +149,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// destination rather than a set of folders it reads.
const [chatDirectory, setChatDirectory] = useState('');
const [chatLinkPreview, setChatLinkPreview] = useState(true);
+ // Whether members' cross-group Search lists this group. Not an app setting:
+ // it is about the group as a whole, and it hides nothing from this page.
+ const [searchListed, setSearchListed] = useState(true);
// MusicBrainz on/off (per-group) — docs/musicbay.md §3.2.
const [musicbrainzConfig, setMusicbrainzConfig] = useState(null);
const onPlayQueue = useCallback((tracks, startIndex) => {
@@ -401,6 +404,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
.map((k) => [k.slice(0, -'_directories'.length), ack[k] || []])));
setChatDirectory(ack.chat_directory || '');
setChatLinkPreview(ack.chat_link_preview !== false);
+ setSearchListed(ack.search_listed !== false);
setMusicbrainzConfig({
enabled: ack.musicbrainz_enabled !== false,
});
@@ -417,6 +421,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
setAppDirectories((prev) => ({ ...prev, [app]: dirs }));
transport.onChatDirectory = (path) => setChatDirectory(path);
transport.onChatLinkPreview = (on) => setChatLinkPreview(on);
+ transport.onSearchListed = (listed) => setSearchListed(listed);
transport.onMusicbrainzEnabled = (enabled) =>
setMusicbrainzConfig((prev) => ({ ...(prev || {}), enabled }));
transport.onRootsChanged = (msg) => {
@@ -865,6 +870,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
onEnabledApps=${(keys) => setEnabledApps(keys)}
scanSettings=${scanSettings}
onScanSettings=${(s) => setScanSettings(s)}
+ searchListed=${searchListed}
entries=${entries} nodeDirs=${nodeDirs}
appSettings=${appSettings}
${/* The saving pane already knows what it asked for; this is so
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index 0f8ccdd..4d67348 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -399,6 +399,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
mnpRoots,
enabledApps, onEnabledApps,
scanSettings, onScanSettings,
+ searchListed,
entries, nodeDirs,
appSettings,
onAppDirectories, onRefreshIndex,
@@ -652,6 +653,27 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
}
}, [transportRef, onScanSettings, reconcileMinutes, debounceSeconds]);
+ // Whether members' cross-group Search lists this group. The switch follows
+ // the node's answer: the ack is broadcast and replayed to the requester
+ // (transport.js BROADCAST_ACK_TYPES), which is what moves `searchListed`.
+ const [listedBusy, setListedBusy] = useState(false);
+ const [listedMsg, setListedMsg] = useState('');
+ const toggleSearchListed = useCallback(async (next) => {
+ const transport = transportRef && transportRef.current;
+ setListedMsg('');
+ setListedBusy(true);
+ try {
+ if (!transport || !transport.connected) {
+ throw new Error('Not connected to the node');
+ }
+ await transport.setSearchListed(next, adminSignFn);
+ } catch (err) {
+ setListedMsg(err.message);
+ } finally {
+ setListedBusy(false);
+ }
+ }, [transportRef, adminSignFn]);
+
// ── Per-app settings ────────────────────────────────────────────────
//
// What every app's settings pane is given, and the one operation the page
@@ -936,6 +958,23 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
${appsMsg && html`<p class="error-msg">${appsMsg}</p>`}
+ ${/* Whether this group shows up in members' cross-group Search. A
+ listing preference, not a permission: the hint says so, because a
+ switch next to "members" and "devices" reads as access control. */
+ isNodeAdmin && connected && html`
+ <${CollapsibleSection} titleKey="settings_node.search_listed_title"
+ defaultOpen=${false}>
+ <div class="settings-row">
+ <${ToggleSwitch} checked=${searchListed !== false}
+ disabled=${listedBusy}
+ onChange=${toggleSearchListed}
+ label=${t('settings_node.search_listed_label')} />
+ </div>
+ <p class="settings-hint">${t('settings_node.search_listed_hint')}</p>
+ ${listedMsg && html`<p class="error-msg">${listedMsg}</p>`}
+ </${CollapsibleSection}>
+ `}
+
${/* How hard the node works watching its own disk — indexer.py
DirectoryIndexer. A performance knob, not a permission: it
changes nothing about who can see or do what. */
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 8607011..4ad8b4b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -710,6 +710,7 @@ export default {
'search.hint': 'Tippen Sie einen Begriff ein, um in allen Ihren Gruppen zu suchen.',
'search.indexing': 'Indexierung von {done} / {total} Gruppen …',
'search.unreachable': { one: '{n} Gruppe nicht erreichbar', other: '{n} Gruppen nicht erreichbar' },
+ 'search.unlisted': { one: '{n} Gruppe wird in der Suche nicht angezeigt – öffnen Sie sie zum Durchsuchen', other: '{n} Gruppen werden in der Suche nicht angezeigt – öffnen Sie sie zum Durchsuchen' },
'search.n_sources': { one: '{n} Quelle', other: '{n} Quellen' },
'search.view_files': 'Dateien',
'search.view_videos': 'Videos',
@@ -889,6 +890,9 @@ export default {
'settings_node.roots': 'Freigegebene Verzeichnisse',
'settings_node.scan_title': 'Scan',
'settings_node.scan_hint': 'Wie oft der Node seine Verzeichnisse erneut auf verpasste Änderungen prüft und wie lange er nach einer Änderung wartet, bevor eine Datei indiziert wird.',
+ 'settings_node.search_listed_title': 'Suche',
+ 'settings_node.search_listed_label': 'Diese Gruppe in der Suche anzeigen',
+ 'settings_node.search_listed_hint': 'Wenn deaktiviert, erscheinen die Dateien dieser Gruppe nicht in der gruppenübergreifenden Suche der Mitglieder. Mitglieder sehen weiterhin alles, wenn sie die Gruppe öffnen: Das nimmt sie nur aus der globalen Auflistung und verbirgt vor niemandem etwas.',
'settings_node.scan_reconcile_label': 'Prüfintervall (Minuten)',
'settings_node.scan_debounce_label': 'Wartezeit nach einer Änderung (Sekunden)',
'settings_node.scan_save': 'Speichern',
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 fee6cc3..43a0db0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -630,6 +630,9 @@ export default {
'settings_node.detach_failed_continue': 'Could not detach the group from the node. Delete on hub anyway?',
'settings_node.scan_title': 'Scanning',
'settings_node.scan_hint': 'How often the node re-checks its directories for changes it may have missed, and how long it waits after a file changes before indexing it.',
+ 'settings_node.search_listed_title': 'Search',
+ 'settings_node.search_listed_label': 'List this group in Search',
+ 'settings_node.search_listed_hint': 'When off, this group\'s files do not appear in members\' cross-group Search. Members still see everything by opening the group: this keeps it out of the global listing and hides nothing from anyone.',
'settings_node.scan_reconcile_label': 'Re-check interval (minutes)',
'settings_node.scan_debounce_label': 'Wait after a change (seconds)',
'settings_node.scan_save': 'Save',
@@ -819,6 +822,7 @@ export default {
'search.hint': 'Type to search file names across all your groups.',
'search.indexing': 'Indexing {done} / {total} groups...',
'search.unreachable': { one: '{n} group unreachable', other: '{n} groups unreachable' },
+ 'search.unlisted': { one: '{n} group is not listed in Search — open it to browse', other: '{n} groups are not listed in Search — open them to browse' },
'search.n_sources': { one: '{n} source', other: '{n} sources' },
'search.view_files': 'Files',
'search.view_videos': 'Videos',
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 b958da3..0df5a38 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -705,6 +705,7 @@ export default {
'search.hint': 'Escriba un término para buscar en todos sus grupos.',
'search.indexing': 'Indexando {done} / {total} grupos…',
'search.unreachable': { one: '{n} grupo inalcanzable', other: '{n} grupos inalcanzables' },
+ 'search.unlisted': { one: '{n} grupo no aparece en la búsqueda: ábrelo para explorarlo', other: '{n} grupos no aparecen en la búsqueda: ábrelos para explorarlos' },
'search.n_sources': { one: '{n} fuente', other: '{n} fuentes' },
'search.view_files': 'Archivos',
'search.view_videos': 'Vídeos',
@@ -884,6 +885,9 @@ export default {
'settings_node.roots': 'Directorios compartidos',
'settings_node.scan_title': 'Escaneo',
'settings_node.scan_hint': 'Con qué frecuencia el node vuelve a comprobar sus directorios en busca de cambios que pudo haber pasado por alto, y cuánto tiempo espera tras un cambio antes de indexar un archivo.',
+ 'settings_node.search_listed_title': 'Búsqueda',
+ 'settings_node.search_listed_label': 'Mostrar este grupo en la búsqueda',
+ 'settings_node.search_listed_hint': 'Si está desactivado, los archivos de este grupo no aparecen en la búsqueda global de los miembros. Los miembros siguen viéndolo todo al abrir el grupo: solo lo saca del listado global y no oculta nada a nadie.',
'settings_node.scan_reconcile_label': 'Intervalo de comprobación (minutos)',
'settings_node.scan_debounce_label': 'Espera tras un cambio (segundos)',
'settings_node.scan_save': 'Guardar',
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 c993d9c..e7dcacd 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -708,6 +708,7 @@ export default {
'search.hint': 'Saisissez un terme pour rechercher dans tous vos groupes.',
'search.indexing': 'Indexation de {done} / {total} groupes…',
'search.unreachable': { one: '{n} groupe injoignable', other: '{n} groupes injoignables' },
+ 'search.unlisted': { one: '{n} groupe n\'est pas affiché dans la recherche — ouvrez-le pour le parcourir', other: '{n} groupes ne sont pas affichés dans la recherche — ouvrez-les pour les parcourir' },
'search.n_sources': { one: '{n} source', other: '{n} sources' },
'search.view_files': 'Fichiers',
'search.view_videos': 'Vidéos',
@@ -902,6 +903,9 @@ export default {
'settings_node.roots': 'Répertoires partagés',
'settings_node.scan_title': 'Analyse',
'settings_node.scan_hint': 'À quelle fréquence le node revérifie ses répertoires à la recherche de changements manqués, et combien de temps il attend après une modification avant d\'indexer un fichier.',
+ 'settings_node.search_listed_title': 'Recherche',
+ 'settings_node.search_listed_label': 'Afficher ce groupe dans la recherche',
+ 'settings_node.search_listed_hint': 'Désactivé, les fichiers de ce groupe n\'apparaissent pas dans la recherche globale des membres. Les membres voient toujours tout en ouvrant le groupe : cela le retire du listage global, sans rien cacher à personne.',
'settings_node.scan_reconcile_label': 'Intervalle de revérification (minutes)',
'settings_node.scan_debounce_label': 'Attente après un changement (secondes)',
'settings_node.scan_save': 'Enregistrer',
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 3d4b502..26de960 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -707,6 +707,7 @@ export default {
'search.hint': 'Digita un termine per cercare in tutti i tuoi gruppi.',
'search.indexing': 'Indicizzazione di {done} / {total} gruppi…',
'search.unreachable': { one: '{n} gruppo non raggiungibile', other: '{n} gruppi non raggiungibili' },
+ 'search.unlisted': { one: '{n} gruppo non è mostrato nella ricerca: aprilo per sfogliarlo', other: '{n} gruppi non sono mostrati nella ricerca: aprili per sfogliarli' },
'search.n_sources': { one: '{n} fonte', other: '{n} fonti' },
'search.view_files': 'File',
'search.view_videos': 'Video',
@@ -898,6 +899,9 @@ export default {
'settings_node.roots': 'Directory condivise',
'settings_node.scan_title': 'Scansione',
'settings_node.scan_hint': 'Con quale frequenza il node ricontrolla le sue directory per cambiamenti che potrebbe aver perso, e quanto tempo attende dopo una modifica prima di indicizzare un file.',
+ 'settings_node.search_listed_title': 'Ricerca',
+ 'settings_node.search_listed_label': 'Mostra questo gruppo nella ricerca',
+ 'settings_node.search_listed_hint': 'Se disattivato, i file di questo gruppo non compaiono nella ricerca globale dei membri. I membri vedono comunque tutto aprendo il gruppo: lo esclude solo dall\'elenco globale e non nasconde nulla a nessuno.',
'settings_node.scan_reconcile_label': 'Intervallo di ricontrollo (minuti)',
'settings_node.scan_debounce_label': 'Attesa dopo una modifica (secondi)',
'settings_node.scan_save': 'Salva',
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 3140527..4db42a1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -695,6 +695,7 @@ export default {
'search.hint': '入力してすべてのグループを検索します。',
'search.indexing': '{done} / {total} グループをインデックス中…',
'search.unreachable': { other: '{n} グループに接続できません' },
+ 'search.unlisted': { other: '{n} グループは検索に表示されません — 開いて閲覧してください' },
'search.n_sources': { other: '{n} 個のソース' },
'search.view_files': 'ファイル',
'search.view_videos': '動画',
@@ -882,6 +883,9 @@ export default {
'settings_node.roots': '共有ディレクトリ',
'settings_node.scan_title': 'スキャン',
'settings_node.scan_hint': 'node が見逃した変更がないかディレクトリを再確認する頻度と、ファイルの変更後にインデックスするまで待つ時間。',
+ 'settings_node.search_listed_title': '検索',
+ 'settings_node.search_listed_label': 'このグループを検索に表示する',
+ 'settings_node.search_listed_hint': 'オフにすると、このグループのファイルはメンバーのグループ横断検索に表示されません。メンバーはグループを開けば引き続きすべて見られます。全体の一覧から外すだけで、誰に対しても何も隠しません。',
'settings_node.scan_reconcile_label': '再確認の間隔(分)',
'settings_node.scan_debounce_label': '変更後の待機時間(秒)',
'settings_node.scan_save': '保存',
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 1e55c8c..a345e94 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -709,6 +709,7 @@ export default {
'search.hint': 'Typ een zoekterm om in al uw groepen te zoeken.',
'search.indexing': '{done} / {total} groepen indexeren…',
'search.unreachable': { one: '{n} groep onbereikbaar', other: '{n} groepen onbereikbaar' },
+ 'search.unlisted': { one: '{n} groep wordt niet getoond in Zoeken — open haar om te bladeren', other: '{n} groepen worden niet getoond in Zoeken — open ze om te bladeren' },
'search.n_sources': { one: '{n} bron', other: '{n} bronnen' },
'search.view_files': 'Bestanden',
'search.view_videos': 'Video\'s',
@@ -900,6 +901,9 @@ export default {
'settings_node.roots': 'Gedeelde mappen',
'settings_node.scan_title': 'Scannen',
'settings_node.scan_hint': 'Hoe vaak de node zijn mappen opnieuw controleert op wijzigingen die zijn gemist, en hoe lang hij na een wijziging wacht voordat een bestand wordt geïndexeerd.',
+ 'settings_node.search_listed_title': 'Zoeken',
+ 'settings_node.search_listed_label': 'Deze groep tonen in Zoeken',
+ 'settings_node.search_listed_hint': 'Uitgeschakeld verschijnen de bestanden van deze groep niet in de groepsoverstijgende zoekfunctie van leden. Leden zien nog steeds alles door de groep te openen: dit haalt haar alleen uit de globale lijst en verbergt niets voor niemand.',
'settings_node.scan_reconcile_label': 'Controle-interval (minuten)',
'settings_node.scan_debounce_label': 'Wachttijd na een wijziging (seconden)',
'settings_node.scan_save': 'Opslaan',
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 8486718..665fd0b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -727,6 +727,7 @@ export default {
'search.hint': 'Wpisz frazę, aby wyszukać we wszystkich grupach.',
'search.indexing': 'Indeksowanie {done} / {total} grup…',
'search.unreachable': { one: '{n} grupa nieosiągalna', few: '{n} grupy nieosiągalne', many: '{n} grup nieosiągalnych', other: '{n} grupy nieosiągalnej' },
+ 'search.unlisted': { one: '{n} grupa nie jest pokazywana w wyszukiwaniu — otwórz ją, aby przeglądać', few: '{n} grupy nie są pokazywane w wyszukiwaniu — otwórz je, aby przeglądać', many: '{n} grup nie jest pokazywanych w wyszukiwaniu — otwórz je, aby przeglądać', other: '{n} grupy nie jest pokazywane w wyszukiwaniu — otwórz je, aby przeglądać' },
'search.n_sources': { one: '{n} źródło', few: '{n} źródła', many: '{n} źródeł', other: '{n} źródła' },
'search.view_files': 'Pliki',
'search.view_videos': 'Filmy',
@@ -926,6 +927,9 @@ export default {
'settings_node.roots': 'Katalogi współdzielone',
'settings_node.scan_title': 'Skanowanie',
'settings_node.scan_hint': 'Jak często node ponownie sprawdza swoje katalogi w poszukiwaniu pominiętych zmian oraz jak długo czeka po zmianie pliku przed jego zindeksowaniem.',
+ 'settings_node.search_listed_title': 'Wyszukiwanie',
+ 'settings_node.search_listed_label': 'Pokazuj tę grupę w wyszukiwaniu',
+ 'settings_node.search_listed_hint': 'Po wyłączeniu pliki tej grupy nie pojawiają się w globalnym wyszukiwaniu członków. Członkowie nadal widzą wszystko po otwarciu grupy: to tylko usuwa ją z globalnej listy i niczego przed nikim nie ukrywa.',
'settings_node.scan_reconcile_label': 'Interwał sprawdzania (minuty)',
'settings_node.scan_debounce_label': 'Oczekiwanie po zmianie (sekundy)',
'settings_node.scan_save': 'Zapisz',
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 e51df81..a85d96e 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
@@ -706,6 +706,7 @@ export default {
'search.hint': 'Digite um termo para pesquisar em todos os seus grupos.',
'search.indexing': 'Indexando {done} / {total} grupos…',
'search.unreachable': { one: '{n} grupo inacessível', other: '{n} grupos inacessíveis' },
+ 'search.unlisted': { one: '{n} grupo não aparece na busca — abra-o para navegar', other: '{n} grupos não aparecem na busca — abra-os para navegar' },
'search.n_sources': { one: '{n} fonte', other: '{n} fontes' },
'search.view_files': 'Arquivos',
'search.view_videos': 'Vídeos',
@@ -885,6 +886,9 @@ export default {
'settings_node.roots': 'Diretórios compartilhados',
'settings_node.scan_title': 'Varredura',
'settings_node.scan_hint': 'Com que frequência o node reverifica seus diretórios em busca de mudanças que possa ter perdido, e quanto tempo espera após uma mudança antes de indexar um arquivo.',
+ 'settings_node.search_listed_title': 'Busca',
+ 'settings_node.search_listed_label': 'Mostrar este grupo na busca',
+ 'settings_node.search_listed_hint': 'Desativado, os arquivos deste grupo não aparecem na busca global dos membros. Os membros continuam vendo tudo ao abrir o grupo: isso só o retira da listagem global e não esconde nada de ninguém.',
'settings_node.scan_reconcile_label': 'Intervalo de reverificação (minutos)',
'settings_node.scan_debounce_label': 'Espera após uma mudança (segundos)',
'settings_node.scan_save': 'Salvar',
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 cc22a70..3e7c1c9 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
@@ -682,6 +682,7 @@ export default {
'search.hint': '输入关键词搜索所有群组。',
'search.indexing': '正在索引 {done} / {total} 个群组…',
'search.unreachable': { other: '{n} 个群组无法连接' },
+ 'search.unlisted': { other: '{n} 个群组未在搜索中显示——打开群组即可浏览' },
'search.n_sources': { other: '{n} 个来源' },
'search.view_files': '文件',
'search.view_videos': '视频',
@@ -869,6 +870,9 @@ export default {
'settings_node.roots': '共享目录',
'settings_node.scan_title': '扫描',
'settings_node.scan_hint': 'node 多久重新检查一次目录以发现可能错过的变化,以及文件变化后等待多久才建立索引。',
+ 'settings_node.search_listed_title': '搜索',
+ 'settings_node.search_listed_label': '在搜索中显示此群组',
+ 'settings_node.search_listed_hint': '关闭后,此群组的文件不会出现在成员的跨群组搜索中。成员打开群组后仍可看到全部内容:这只是将其移出全局列表,不对任何人隐藏任何内容。',
'settings_node.scan_reconcile_label': '重新检查间隔(分钟)',
'settings_node.scan_debounce_label': '变化后的等待时间(秒)',
'settings_node.scan_save': '保存',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
index ee99ccc..a77e8c3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
@@ -32,6 +32,51 @@ const SEARCH_VIDEO_ROOT = '__search__';
const SEARCH_AUDIO_ROOT = '__search__';
const SEARCH_PHOTO_ROOTS = ['__search_photos__'];
+// -- Connecting to a group ----------------------------------------------------
+
+/**
+ * A live connection to one of the nodes serving `groupId`, and its ack — or
+ * null when the hub lists no node at all.
+ *
+ * Every node the hub lists, in turn, exactly as group-page.js does since
+ * 2026-09-11: the list is in hub registration order, and its head is not
+ * necessarily a node that answers. Search took `nodes[0]` and stopped, so a
+ * group with a second, working node counted as unreachable here while it
+ * opened fine from the sidebar. A refusal naming a state of this browser (a
+ * code, a passphrase, a device) is the same from every node and stops the
+ * walk; `not_hosted`, a timeout or a failed connection moves on.
+ */
+async function connectToGroup(hubBase, groupId, token, bundleKey, username, userId) {
+ const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token });
+ if (!nodesData.nodes || !nodesData.nodes.length) return null;
+
+ const live = (await ensureFreshToken()) || token;
+ let lastErr = null;
+ for (const n of nodesData.nodes) {
+ const transport = new window.MeshBayTransport(hubBase, live);
+ transport.onNeedToken = async () => (await ensureFreshToken()) || token;
+ let timer;
+ try {
+ const ack = await Promise.race([
+ transport.connect(
+ n.node_id, live, groupId, null, null, bundleKey,
+ username, userId, null),
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error('Connection timeout')), SEARCH_TIMEOUT);
+ }),
+ ]);
+ clearTimeout(timer);
+ return { transport, ack };
+ } catch (e) {
+ clearTimeout(timer);
+ lastErr = e;
+ try { transport.close(); } catch {}
+ if (e.reason && e.reason !== 'not_hosted') throw e;
+ }
+ }
+ throw (lastErr || new Error('no node served this group'));
+}
+
// -- Connection pool ----------------------------------------------------------
class ConnectionPool {
@@ -66,29 +111,10 @@ class ConnectionPool {
}
async _doConnect(groupId, token, bundleKey, username, userId) {
- const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token });
- if (!nodesData.nodes || !nodesData.nodes.length) throw new Error('offline');
-
- const live = (await ensureFreshToken()) || token;
- const transport = new window.MeshBayTransport(this._hubBase, live);
- transport.onNeedToken = async () => (await ensureFreshToken()) || token;
-
- let timer;
- try {
- await Promise.race([
- transport.connect(
- nodesData.nodes[0].node_id, live, groupId, null, null, bundleKey,
- username, userId, null),
- new Promise((_, reject) => {
- timer = setTimeout(() => reject(new Error('Connection timeout')), SEARCH_TIMEOUT);
- }),
- ]);
- clearTimeout(timer);
- } catch (e) {
- clearTimeout(timer);
- try { transport.close(); } catch {}
- throw e;
- }
+ const found = await connectToGroup(
+ this._hubBase, groupId, token, bundleKey, username, userId);
+ if (!found) throw new Error('offline');
+ const { transport } = found;
let gek = null;
if (transport.gekRaw && window.MeshBayCrypto) {
@@ -128,24 +154,17 @@ class ConnectionPool {
// -- Index fetching -----------------------------------------------------------
async function fetchGroupIndex(groupId, token, bundleKey, username, userId) {
- const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token });
- if (!nodesData.nodes || !nodesData.nodes.length) return null;
-
- const live = (await ensureFreshToken()) || token;
- const transport = new window.MeshBayTransport(HUB, live);
- transport.onNeedToken = async () => (await ensureFreshToken()) || token;
+ const found = await connectToGroup(HUB, groupId, token, bundleKey, username, userId);
+ if (!found) return null;
+ const { transport, ack } = found;
try {
- let timer;
- const ack = await Promise.race([
- transport.connect(
- nodesData.nodes[0].node_id, live, groupId, null, null, bundleKey,
- username, userId, null),
- new Promise((_, reject) => {
- timer = setTimeout(() => reject(new Error('Connection timeout')), SEARCH_TIMEOUT);
- }),
- ]);
- clearTimeout(timer);
+ // The operator asked for this group to stay out of the global listing.
+ // Decided here, before the index is asked for, so nothing of it is held,
+ // cached or merged by this page. A listing preference and not a boundary:
+ // the node cannot tell this request from the group page's, and opening the
+ // group lists everything.
+ if (ack.search_listed === false) return { unlisted: true };
const indexMsg = await transport.fetchIndex();
// Plural, with the old scalars as the fallback for a node still speaking
@@ -174,6 +193,7 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onBa
const total = groups.length;
let done = 0;
const unreachable = [];
+ const unlisted = [];
const results = new Map();
for (let i = 0; i < groups.length; i += BATCH_SIZE) {
@@ -181,7 +201,9 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onBa
await Promise.all(batch.map(async (g) => {
try {
const result = await fetchGroupIndex(g.id, token, bundleKey, username, userId);
- if (result) {
+ if (result && result.unlisted) {
+ unlisted.push(g.name || g.id);
+ } else if (result) {
results.set(g.id, {
...result,
groupName: g.name,
@@ -195,11 +217,11 @@ async function fetchAllIndexes(groups, token, username, userId, onProgress, onBa
unreachable.push(g.name || g.id);
}
done++;
- onProgress({ done, total, unreachable: [...unreachable] });
+ onProgress({ done, total, unreachable: [...unreachable], unlisted: [...unlisted] });
}));
onBatch(new Map(results));
}
- return { results, unreachable };
+ return { results, unreachable, unlisted };
}
// -- SearchPage ---------------------------------------------------------------
@@ -281,7 +303,8 @@ function photoUnits(entries) {
function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs }) {
const [indexedGroups, setIndexedGroups] = useState(new Map());
- const [progress, setProgress] = useState({ done: 0, total: 0, unreachable: [] });
+ const [progress, setProgress] = useState(
+ { done: 0, total: 0, unreachable: [], unlisted: [] });
const [fetching, setFetching] = useState(false);
// Bumped by the Files breadcrumb refresh button — re-runs the all-groups
// index fetch below, the only "cache" this page has.
@@ -333,7 +356,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
(async () => {
setFetching(true);
- setProgress({ done: 0, total: groups.length, unreachable: [] });
+ setProgress({ done: 0, total: groups.length, unreachable: [], unlisted: [] });
await fetchAllIndexes(
groups, token, username, userId,
@@ -718,6 +741,12 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
</p>
`}
+ ${!fetching && progress.unlisted.length > 0 && html`
+ <p class="search-unreachable">
+ ${t('search.unlisted', { n: progress.unlisted.length })}
+ </p>
+ `}
+
${viewMode === 'files' && hasResults && html`
<${FilesPanel}
groupId="search"
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 9de5407..1f05b8d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -96,7 +96,7 @@ const BROADCAST_ACK_TYPES = new Set([
'root_update_ack', 'root_eject_ack', 'root_plug_ack',
'root_add_ack', 'root_remove_ack',
'app_directories_ack', 'chat_directory_ack', 'chat_link_preview_ack',
- 'chat_epoch_ack',
+ 'chat_epoch_ack', 'search_listed_ack',
]);
/** Hand a broadcast ack to the callback that would have had it from a peer. */
@@ -107,6 +107,8 @@ function _replayBroadcast(transport, msg) {
transport._onChatDirectory(msg.path || '');
} else if (msg.type === 'chat_link_preview_ack' && transport._onChatLinkPreview) {
transport._onChatLinkPreview(Boolean(msg.enabled));
+ } else if (msg.type === 'search_listed_ack' && transport._onSearchListed) {
+ transport._onSearchListed(msg.listed !== false);
} else if (msg.type === 'chat_epoch_ack') {
transport._applyChatEpoch(msg);
} else if (transport._onRootsChanged) {
@@ -120,7 +122,7 @@ const ADMIN_OP_TYPES = new Set([
'apps_enabled', 'set_scan_settings', 'member_revoke',
'root_add', 'root_remove', 'root_update', 'root_eject', 'root_plug',
'app_directories', 'chat_directory', 'chat_link_preview', 'chat_epoch',
- 'member_unpin', 'gek_rotate', 'group_attach',
+ 'search_listed', 'member_unpin', 'gek_rotate', 'group_attach',
'group_detach', 'invite_create',
]);
@@ -533,6 +535,7 @@ class MeshBayTransport {
set onAppDirectories(fn) { this._onAppDirectories = fn; }
set onChatDirectory(fn) { this._onChatDirectory = fn; }
set onChatLinkPreview(fn) { this._onChatLinkPreview = fn; }
+ set onSearchListed(fn) { this._onSearchListed = fn; }
set onChatEpoch(fn) { this._onChatEpoch = fn; }
set onTmdbConfig(fn) { this._onTmdbConfig = fn; }
set onTmdbEnabled(fn) { this._onTmdbEnabled = fn; }
@@ -1526,6 +1529,22 @@ class MeshBayTransport {
}
/**
+ * Whether this group's files appear in members' cross-group Search.
+ * Presentation only — opening the group lists everything regardless.
+ */
+ async setSearchListed(listed, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'search_listed', v: '3.0', listed: Boolean(listed),
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(
+ msg, 'search_listed', listed ? 'on' : 'off', signFn);
+ }
+ return msg;
+ }
+
+ /**
* Open a new chat epoch by hand. Operator only, and signed.
*
* Not a switch — there is nothing to turn on. The removals that matter open
@@ -3097,6 +3116,9 @@ class MeshBayTransport {
if (msg.type === 'chat_link_preview_ack' && this._onChatLinkPreview) {
this._onChatLinkPreview(Boolean(msg.enabled));
}
+ if (msg.type === 'search_listed_ack' && this._onSearchListed) {
+ this._onSearchListed(msg.listed !== false);
+ }
if (msg.type === 'chat_epoch_ack') {
this._applyChatEpoch(msg);
return;