diff options
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; |