diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-14 21:45:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-14 21:45:53 +0200 |
| commit | cd2745cecff12e894e0dfa702bff6a90f0e8734e (patch) | |
| tree | 8e864603cfd4c49cde535c8c4f96c5151ed4276c /packages/meshbay-hub/src | |
| parent | df3b808792daa745b5b0d5b9896ddca8849fe8b1 (diff) | |
| download | meshbay-cd2745cecff12e894e0dfa702bff6a90f0e8734e.tar.gz | |
feat: a group can be left out of Search, and Search tries every node
`search_listed` is a per-group setting on the node, changed by a signed
operator op and carried in the sealed handshake ack. Search reads it after
the handshake and stops there: no index is fetched, cached or merged, in any
of the four views, and the page says how many groups it left out. The switch
is a "Search" section in the group's settings, shown to the operator.
Absent means listed, at every layer: roster default, ack default, and the
client only drops a group on an explicit `false` — so an upgrade or an older
node removes nothing from anyone's Search.
It is a listing preference and protects nothing: the node serves the same
index to Search and to the group page and cannot tell them apart, every
member lists the group by opening it, and a client that ignores the flag
lists it in Search too. Design §9.11 says so, so it is never described as
private. The cost is one handshake per unlisted group, because only the node
knows the setting.
Search also took `nodes[0]` twice — for the index and for the pooled
connection — the defect 4cce50f fixed on the group page only. One
`connectToGroup` now walks the list the same way: a refusal about this
browser stops, `not_hosted` or a failed connection moves on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm
Diffstat (limited to 'packages/meshbay-hub/src')
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; |