diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
15 files changed, 202 insertions, 11 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index c5f2733..bdab271 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -21,6 +21,7 @@ import { } from './hub-client.js'; import { startIdleWatch, markActive } from './idle.js'; import { GroupPage } from './group-page.js'; +import { parseGroupLink, groupLinkRoute, findLinkedGroup } from './group-link.js'; import { lazy } from './lazy.js'; import { ConnectionPool } from './connection-pool.js'; import { MusicPlayerBar } from './music-player.js'; @@ -766,6 +767,9 @@ function App() { const [needsHub, setNeedsHub] = useState( platform.isNative && !platform.hubBase()); const [groups, setGroups] = useState([]); + // Whether `/mine` has answered for this sign-in. A `#/name@owner` link can + // only be called unknown once the list it is looked up in has arrived. + const [groupsLoaded, setGroupsLoaded] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); @@ -1008,11 +1012,13 @@ function App() { if (!user) { nodeKeyAskedForRef.current = null; setGroups([]); setNotifications([]); setUnreadCount(0); setHasNodeKey(false); + setGroupsLoaded(false); return; } hubFetch('/v1/groups/mine', { token: user.token }) .then(data => setGroups(data.groups || [])) - .catch(() => setGroups([])); + .catch(() => setGroups([])) + .finally(() => setGroupsLoaded(true)); hubFetch('/v1/users/me/preferences', { token: user.token }) .then(async (prefs) => { setUserPrefs(prefs || {}); @@ -1233,6 +1239,31 @@ function App() { // out as the only way back. const refreshAuth = useCallback(() => refreshAccessToken(), []); + // A group is reached by its id (`#/group/<id>`, what every link inside the + // application uses) or by its handle (`#/name@owner[/path]`, the one a person + // types or shares — group-link.js). Both open the same page; `groupRoute` is + // the first form whichever was used, so the sidebar and the page see one. + const groupLink = route.startsWith('/group/') ? null : parseGroupLink(route); + const linkedGroup = groupLink ? findLinkedGroup(groups, groupLink) : null; + const groupId = route.startsWith('/group/') ? route.slice(7) + : linkedGroup ? linkedGroup.id : null; + const groupRoute = groupId ? '/group/' + groupId : route; + const shownGroup = groupId ? groups.find(g => g.id === groupId) : null; + const linkPath = groupLink && linkedGroup ? groupLink.path : ''; + // The address shows the handle, which is the link worth copying. `replace`, + // so this is not a history entry; and only once the path a link named has + // been acted on (`onLinkOpened`), so a reload does not download the file a + // second time. + const shownGroupRoute = shownGroup ? groupLinkRoute(shownGroup) : null; + useEffect(() => { + if (shownGroupRoute && !linkPath && route !== shownGroupRoute) { + window.location.replace('#' + shownGroupRoute); + } + }, [route, shownGroupRoute, linkPath]); + const onLinkOpened = useCallback(() => { + if (shownGroupRoute) window.location.replace('#' + shownGroupRoute); + }, [shownGroupRoute]); + let page; // A desktop build with no hub configured cannot do anything at all, so it // asks before showing a sign-in form that could not work. The field comes @@ -1282,17 +1313,23 @@ function App() { }} />`; } else if (route === '/node' && platform.capabilities.nodeAdmin && hasNodeKey) { page = html`<${LazyNodePage} groups=${groups} token=${user.token} username=${user.username} />`; - } else if (route.startsWith('/group/')) { - const groupId = route.slice(7); - const group = groups.find(g => g.id === groupId); + } else if (groupId) { page = html`<${GroupPage} - groupId=${groupId} group=${group} token=${user.token} + groupId=${groupId} group=${shownGroup} token=${user.token} + openPath=${linkPath} onLinkOpened=${onLinkOpened} username=${user.username} userId=${user.userId} userPrefs=${userPrefs} onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications} onGroupUpdated=${updateGroup} onPresence=${notePresence} onLeft=${handleLeftGroup} onPlayQueue=${handlePlayQueue} onStopMusic=${handleStopMusic} />`; + } else if (groupLink) { + // Not among this account's groups — or not yet known to be. The same words + // whether it does not exist or is someone else's: the list it was looked + // up in is this account's own, so there is nothing else to tell. + page = groupsLoaded + ? html`<div class="page-content"><p class="page-message">${t('group.link_unknown')}</p></div>` + : html`<div class="page-content"><p class="page-message"><span class="spinner"></span></p></div>`; } else if (route === '/admin') { page = (user.role === 'moderator' || user.role === 'admin') ? html`<${LazyAdminPage} token=${user.token} role=${user.role} />` @@ -1334,7 +1371,7 @@ function App() { groups=${groups} presence=${presence} indexProgressPct=${indexProgressPct} - route=${route} + route=${groupRoute} menuOpen=${menuOpen} role=${user.role} allowPublicGroups=${allowPublicGroups} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js index c12db72..57db475 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -185,8 +185,12 @@ export function LoginPage({ onLogin }) { // Trimmed to match the hub's stored username and every client-side key // derivation (auth_key, bundle_key, recovery_key all fold the username in). await onLogin(name, password); - // Back to the invitation that sent them here, if one is waiting. - navigate(loadPending() ? '/invite' : '/'); + // Back to the invitation that sent them here, if one is waiting. Otherwise + // nowhere: on `#/login` or `#/register` the router sends a signed-in + // person home itself, and anywhere else this form stood in for the page + // the address names — a group, a file in one — which is where they were + // going. Sending everyone home lost every link opened signed out. + if (loadPending()) navigate('/invite'); } catch (err) { if (err.message === 'email_verification_required') { setPendingVerif(true); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js index 38157b9..978090c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js @@ -142,12 +142,16 @@ function FilesPanel({ entries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex, isNodeAdmin, operatorPaired, userId, setError, onPreview, showGroup, readOnly, getTransport, onRefreshIndex, showRefresh, onReport, + openDirectory, }) { const [selected, setSelected] = useState(() => new Set()); const [sortKey, setSortKey] = useState('name'); const [sortAsc, setSortAsc] = useState(true); const [filter, setFilter] = useState(''); - const [currentPath, setCurrentPath] = useState(''); + const [currentPath, setCurrentPath] = useState(() => (openDirectory ? openDirectory.dir : '')); + // A link into the group named a folder (group-page.js); a new object each + // time, so the same folder linked twice is opened twice. + useEffect(() => { if (openDirectory) setCurrentPath(openDirectory.dir); }, [openDirectory]); const [refreshing, setRefreshing] = useState(false); // The toolbar pins below the page's own band and tells the column heads how // far down to pin. Its height is not a constant — it wraps to three rows on diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-link.js b/packages/meshbay-hub/src/meshbay_hub/static/group-link.js new file mode 100644 index 0000000..3c9063c --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-link.js @@ -0,0 +1,95 @@ +/** + * A group named in the address: `#/name@owner`, optionally followed by a path + * inside it — `#/name@owner/root/folder/file.jpg`. + * + * The same handle `GroupName` shows under every group, so a link reads the way + * the group is labelled. It is resolved against the signed-in account's own + * group list and nothing else: no hub route answers "which group is this + * name", so a name says nothing to someone who is not already a member, and + * nobody can probe for one. The UUID stays the group's identity; a renamed + * group breaks its name links, never its `#/group/<id>` ones. + * + * In the fragment, never the path: what follows `#` is not sent to the server, + * so a group's name and a file's path appear in no hub or proxy log and in no + * Referer. + * + * The owner is everything after the *last* `@`: a username cannot contain one + * (users.py, RegisterRequest), a group name can. Each segment is + * percent-decoded on its own, so a `/` inside a name or a file name travels as + * `%2F` without splitting the path. + * + * No imports, so `test_group_link.py` can execute the module as it is. + */ + +function _decode(segment) { + try { return decodeURIComponent(segment); } catch { return null; } +} + +// `@` is legal in a fragment and the owner is found by the last one, so the +// name's own need no escaping; everything else is escaped as a URI component. +function _encode(segment) { + return encodeURIComponent(segment).replace(/%40/g, '@'); +} + +/** + * `{ name, owner, path }` for a route (the hash without its `#`), or null when + * the route does not name a group. `path` is '' for the group itself. + */ +function parseGroupLink(route) { + if (!route || route[0] !== '/') return null; + const parts = route.slice(1).split('/'); + const head = _decode(parts[0]); + if (!head) return null; + const at = head.lastIndexOf('@'); + if (at <= 0 || at === head.length - 1) return null; + const rest = parts.slice(1).filter(Boolean).map(_decode); + if (rest.some((s) => s === null || s === '.' || s === '..')) return null; + return { name: head.slice(0, at), owner: head.slice(at + 1), path: rest.join('/') }; +} + +/** The route naming `group`, and `path` inside it when one is given. */ +function groupLinkRoute(group, path = '') { + const head = `/${_encode(group.name)}@${_encode(group.owner_username || '')}`; + const tail = path ? '/' + path.split('/').filter(Boolean).map(_encode).join('/') : ''; + return head + tail; +} + +/** + * The group `link` names among `groups` (`/v1/groups/mine` rows), or null. + * + * Case-insensitively on the name, as the hub keeps it unique + * (`uq_groups_owner_name` is on `lower(name)`); the owner as typed first, then + * case-insensitively when that finds exactly one. + */ +function findLinkedGroup(groups, link) { + if (!link) return null; + const name = link.name.toLowerCase(); + const named = (groups || []).filter((g) => (g.name || '').toLowerCase() === name); + const exact = named.find((g) => g.owner_username === link.owner); + if (exact) return exact; + const owner = link.owner.toLowerCase(); + const loose = named.filter((g) => (g.owner_username || '').toLowerCase() === owner); + return loose.length === 1 ? loose[0] : null; +} + +/** + * What `path` names in a group's index: `{ kind: 'file', entry }`, + * `{ kind: 'dir', dir }`, or null. An entry's `path` is its folder, root + * first, and `name` its file name — so a file is matched on both together. + * Folders come from the files under them and from the node's own listing, + * which is the only place an empty one appears. + */ +function resolveLinkedPath(entries, nodeDirs, path) { + if (!path) return null; + const cut = path.lastIndexOf('/'); + const dir = cut < 0 ? '' : path.slice(0, cut); + const name = cut < 0 ? path : path.slice(cut + 1); + const entry = (entries || []).find((e) => (e.path || '') === dir && e.name === name); + if (entry) return { kind: 'file', entry }; + const prefix = path + '/'; + const isDir = (nodeDirs || []).includes(path) + || (entries || []).some((e) => (e.path || '') === path || (e.path || '').startsWith(prefix)); + return isDir ? { kind: 'dir', dir: path } : null; +} + +export { parseGroupLink, groupLinkRoute, findLinkedGroup, resolveLinkedPath }; 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 fe858b2..a2d6c18 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -5,6 +5,7 @@ import { t } from './i18n.js'; import { Icon } from './icon.js'; import { transfers } from './transfers.js'; import { downloadEntry } from './file-utils.js'; +import { resolveLinkedPath } from './group-link.js'; import { HUB, session, hubFetch, ensureFreshToken, _loadBundleKey, _loadRecoveryKey, _storeBundleKey, @@ -33,7 +34,8 @@ import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; */ function GroupPage({ groupId, group, token, username, userId, userPrefs, onRefreshAuth, onJoined, onGroupUpdated, onPresence, onLeft, - onPlayQueue: parentOnPlayQueue, onStopMusic }) { + onPlayQueue: parentOnPlayQueue, onStopMusic, + openPath = '', onLinkOpened }) { const [status, setStatus] = useState('idle'); // The tab bar pins under the navigation bar and tells the application's own // toolbar how far down to pin (style.css, "Sticky chrome"). @@ -313,12 +315,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, }); }, []); + // Which group `entries` is the index of. The page is not remounted between + // groups, so for one render after a switch `status` and `entries` are still + // the last group's — and a link into the new one must not be looked up there. + const indexGroupRef = useRef(null); + // One place that takes an index from the node and puts it everywhere it has to // go. Deleting a file used to refresh the table and leave the cache alone, so // the search page went on offering a file that no longer existed until the // group was reconnected. const applyIndex = useCallback((indexMsg) => { const fresh = indexMsg.entries || []; + indexGroupRef.current = groupId; setEntries(fresh); if (indexMsg.dirs) setNodeDirs(indexMsg.dirs); if (indexMsg.roots) setNodeRoots(indexMsg.roots); @@ -731,6 +739,28 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, await downloadEntry(transfers, transport, gekRef.current, entry); }, []); + // A link that named a path inside the group (`#/name@owner/root/dir/file`, + // group-link.js): a file is downloaded, and Files opens on its folder either + // way. Acted on once the index is in — before that, nothing can say whether + // the path is a file, a folder or nothing — and then handed back to the + // router, which takes the path out of the address so a reload does not + // download it again. + const [openDirectory, setOpenDirectory] = useState(null); + useEffect(() => { setOpenDirectory(null); }, [groupId]); + useEffect(() => { + if (!openPath || status !== 'connected' || indexGroupRef.current !== groupId) return; + const target = resolveLinkedPath(entries, nodeDirs, openPath); + if (!target) { + setError(t('group.link_path_unknown', { path: openPath })); + } else { + const dir = target.kind === 'file' ? (target.entry.path || '') : target.dir; + setOpenDirectory({ dir }); + chooseTab('files'); + if (target.kind === 'file') downloadFileForModal(target.entry); + } + if (onLinkOpened) onLinkOpened(); + }, [openPath, status, groupId]); + const refreshIndex = useCallback(async () => { const transport = transportRef.current; if (!transport || !transport.connected) return; @@ -987,7 +1017,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, </div> ${apps.map(a => tab === a.key && html` - <${a.Component} key=${a.key + '-' + groupId} ...${commonProps} /> + <${a.Component} key=${a.key + '-' + groupId} ...${commonProps} + ...${a.key === 'files' ? { openDirectory } : {}} /> `)} ${tab === 'settings' && html` 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 5cc9968..8d14dca 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -210,6 +210,8 @@ export default { 'group.mkdir_prompt': 'Name des neuen Ordners', 'group.mkdir_offline': 'Nicht mit dem Node verbunden.', 'group.download_offline': 'Keine Verbindung zum Node — der Download kann nicht starten. Die Verbindung wird automatisch wiederhergestellt; versuchen Sie es gleich erneut.', + 'group.link_unknown': "Dieser Link verweist auf eine Gruppe, in der Sie nicht Mitglied sind, oder die nicht mehr so heißt.", + 'group.link_path_unknown': "Unter {path} gibt es in dieser Gruppe nichts — die Datei wurde vielleicht verschoben, umbenannt oder gelöscht.", 'group.download_write_stalled': 'Die Datei wird nicht mehr auf die Festplatte geschrieben ({seconds} s ohne Fortschritt). Der Download wurde abgebrochen statt hängen gelassen; versuchen Sie es erneut.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 5aa53b7..a6cd0b4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -210,6 +210,8 @@ export default { 'group.mkdir_prompt': 'New folder name', 'group.mkdir_offline': 'Not connected to the node.', 'group.download_offline': 'Not connected to the node — the download cannot start. It reconnects on its own; try again in a moment.', + 'group.link_unknown': "This link names a group you are not a member of, or one that no longer goes by that name.", + 'group.link_path_unknown': "Nothing at {path} in this group — the file may have been moved, renamed or deleted.", 'group.download_write_stalled': 'The file stopped being written to disk ({seconds}s with no progress). The download was stopped rather than left hanging; try it again.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 6558a46..e066081 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -208,6 +208,8 @@ export default { 'group.mkdir_prompt': 'Nombre de la nueva carpeta', 'group.mkdir_offline': 'Sin conexión con el nodo.', 'group.download_offline': 'Sin conexión con el nodo — la descarga no puede empezar. Se reconecta sola; inténtelo de nuevo en un momento.', + 'group.link_unknown': "Este enlace nombra un grupo del que no eres miembro, o que ya no se llama así.", + 'group.link_path_unknown': "No hay nada en {path} en este grupo: puede que el archivo se haya movido, renombrado o eliminado.", 'group.download_write_stalled': 'El archivo dejó de escribirse en el disco ({seconds} s sin avance). La descarga se detuvo en lugar de quedarse colgada; inténtelo de nuevo.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 5e4c0bf..01ccfcd 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -209,6 +209,8 @@ export default { 'group.mkdir_prompt': 'Nom du nouveau dossier', 'group.mkdir_offline': 'Non connecté au nœud.', 'group.download_offline': 'Pas de connexion au node — le téléchargement ne peut pas démarrer. La reconnexion est automatique, réessayez dans un instant.', + 'group.link_unknown': "Ce lien désigne un groupe dont vous n'êtes pas membre, ou qui ne porte plus ce nom.", + 'group.link_path_unknown': "Rien à l'emplacement {path} dans ce groupe — le fichier a peut-être été déplacé, renommé ou supprimé.", 'group.download_write_stalled': 'L\'écriture du fichier sur le disque s\'est arrêtée ({seconds} s sans progression). Le téléchargement a été interrompu plutôt que laissé en suspens ; réessayez.', 'device.add_title': 'Ce navigateur n’est pas encore lié à ce nœud', 'device.add_hint': 'Votre compte est connu ici, mais ce navigateur détient une autre clé. Approuvez-le depuis un appareil déjà lié — sans passer par l’opérateur.', 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 8772376..71af9f8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -209,6 +209,8 @@ export default { 'group.mkdir_prompt': 'Nome della nuova cartella', 'group.mkdir_offline': 'Non connesso al nodo.', 'group.download_offline': 'Nessuna connessione al nodo — il download non può iniziare. La riconnessione è automatica, riprovi tra poco.', + 'group.link_unknown': "Questo link indica un gruppo di cui non sei membro, o che non ha più questo nome.", + 'group.link_path_unknown': "Non c'è nulla in {path} in questo gruppo: il file potrebbe essere stato spostato, rinominato o eliminato.", 'group.download_write_stalled': 'Il file ha smesso di essere scritto su disco ({seconds} s senza progressi). Il download è stato interrotto invece di restare bloccato; riprovi.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 fa34470..4b357c8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -207,6 +207,8 @@ export default { 'group.mkdir_prompt': '新しいフォルダー名', 'group.mkdir_offline': 'ノードに接続していません。', 'group.download_offline': 'ノードに接続していません — ダウンロードを開始できません。再接続は自動で行われます。少し待って再試行してください。', + 'group.link_unknown': "このリンクは、あなたがメンバーでないグループ、またはもうその名前ではないグループを指しています。", + 'group.link_path_unknown': "このグループの {path} には何もありません。ファイルが移動、名前変更、または削除された可能性があります。", 'group.download_write_stalled': 'ファイルのディスクへの書き込みが止まりました({seconds} 秒間進みません)。ぶら下がったままにせず中止しました。もう一度お試しください。', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 fa0841d..1f4b12c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -210,6 +210,8 @@ export default { 'group.mkdir_prompt': 'Naam van de nieuwe map', 'group.mkdir_offline': 'Niet verbonden met de node.', 'group.download_offline': 'Geen verbinding met de node — de download kan niet starten. Er wordt automatisch opnieuw verbonden; probeer het zo weer.', + 'group.link_unknown': "Deze link verwijst naar een groep waarvan je geen lid bent, of die niet meer zo heet.", + 'group.link_path_unknown': "Er staat niets op {path} in deze groep — het bestand is misschien verplaatst, hernoemd of verwijderd.", 'group.download_write_stalled': 'Het bestand wordt niet meer naar schijf geschreven ({seconds} s zonder voortgang). De download is gestopt in plaats van te blijven hangen; probeer het opnieuw.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 9de29ea..05a3006 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -213,6 +213,8 @@ export default { 'group.mkdir_prompt': 'Nazwa nowego folderu', 'group.mkdir_offline': 'Brak połączenia z węzłem.', 'group.download_offline': 'Brak połączenia z węzłem — pobieranie nie może się rozpocząć. Połączenie wróci samo; proszę spróbować za chwilę.', + 'group.link_unknown': "Ten link wskazuje grupę, do której nie należysz, lub która nie nosi już tej nazwy.", + 'group.link_path_unknown': "W tej grupie nie ma niczego pod {path} — plik mógł zostać przeniesiony, zmieniony lub usunięty.", 'group.download_write_stalled': 'Plik przestał być zapisywany na dysk ({seconds} s bez postępu). Pobieranie zostało przerwane, zamiast wisieć w nieskończoność; proszę spróbować ponownie.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 908a8db..824c69a 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 @@ -210,6 +210,8 @@ export default { 'group.mkdir_prompt': 'Nome da nova pasta', 'group.mkdir_offline': 'Sem conexão com o nó.', 'group.download_offline': 'Sem conexão com o nó — o download não pode começar. Ele reconecta sozinho; tente de novo em instantes.', + 'group.link_unknown': "Este link aponta para um grupo do qual você não é membro, ou que não tem mais esse nome.", + 'group.link_path_unknown': "Não há nada em {path} neste grupo — o arquivo pode ter sido movido, renomeado ou excluído.", 'group.download_write_stalled': 'O arquivo parou de ser gravado no disco ({seconds}s sem progresso). O download foi interrompido em vez de ficar travado; tente de novo.', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', 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 d6fe782..d0d3954 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 @@ -206,6 +206,8 @@ export default { 'group.mkdir_prompt': '新文件夹名称', 'group.mkdir_offline': '未连接到节点。', 'group.download_offline': '未连接到节点 — 无法开始下载。连接会自动恢复,请稍后重试。', + 'group.link_unknown': "此链接指向一个您不是其成员的群组,或该群组已不再使用此名称。", + 'group.link_path_unknown': "此群组中 {path} 处没有内容——文件可能已被移动、重命名或删除。", 'group.download_write_stalled': '文件停止写入磁盘({seconds} 秒无进展)。已中止下载而不是让它一直卡住,请重试。', 'device.add_title': 'This browser is not linked to this node yet', 'device.add_hint': 'Your account is known here, but this browser holds a different key. Approve it from a device already linked — no operator needed.', |